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
71 changes: 30 additions & 41 deletions src/app/wallets/add-invoice-for-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toSats } from "@domain/bitcoin"
import { checkedToWalletId } from "@domain/wallets"
import { RateLimitConfig } from "@domain/rate-limit"
import { checkedToMinutes } from "@domain/primitives"
import { UnsupportedCurrencyError } from "@domain/errors"
import { RateLimiterExceededError } from "@domain/rate-limit/errors"
import { DEFAULT_EXPIRATIONS } from "@domain/bitcoin/lightning/invoice-expiration"
import { WalletInvoiceBuilder } from "@domain/wallet-invoices/wallet-invoice-builder"
Expand All @@ -16,13 +17,14 @@ import {
WalletsRepository,
} from "@services/mongoose"

import { validateIsBtcWallet, validateIsUsdWallet } from "./validate"
import Ibex from "@services/ibex/client"
import { IbexError, UnexpectedIbexResponse } from "@services/ibex/errors"
import { decodeInvoice } from "@domain/bitcoin/lightning/ln-invoice"
import { checkedToUsdPaymentAmount, USDAmount, UsdPaymentAmount, ValidationError } from "@domain/shared"
import { USDAmount } from "@domain/shared"
import { AddInvoiceResponse201 } from "ibex-client"

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

const defaultBtcExpiration = DEFAULT_EXPIRATIONS["BTC"].delayMinutes
const defaultUsdExpiration = DEFAULT_EXPIRATIONS["USD"].delayMinutes

Expand All @@ -34,7 +36,7 @@ const addInvoiceForSelf = async ({
}: AddInvoiceForSelfArgs): Promise<LnInvoice | ApplicationError> => {
const wallet = await WalletsRepository().findById(walletId)
if (wallet instanceof Error) return wallet

const account = await AccountsRepository().findById(wallet.accountId)
if (account instanceof Error) return account

Expand All @@ -44,32 +46,27 @@ 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 ? USDAmount.cents(amount.toString()) : undefined
if (checkedAmount instanceof Error) return checkedAmount
const resp = await Ibex.addInvoice({
amount: checkedAmount,
amount: checkedAmount,
accountId: walletId,
memo,
expiration: expiresIn * 60 as Seconds,
expiration: (expiresIn * 60) as Seconds,
})
if (resp instanceof IbexError) return resp
return toDomainInvoice(resp)
}

// Flash fork: remove because not BTC Wallets
// export const addInvoiceForSelfForBtcWallet = async (
// args: AddInvoiceForSelfForBtcWalletArgs,
// ): Promise<LnInvoice | ApplicationError> => {
// const walletId = checkedToWalletId(args.walletId)
// if (walletId instanceof Error) return walletId

// const expiresIn = checkedToMinutes(args.expiresIn || defaultBtcExpiration)
// if (expiresIn instanceof Error) return expiresIn

// const validated = await validateIsBtcWallet(walletId)
// if (validated instanceof Error) return validated
// return addInvoiceForSelf({ ...args, walletId, expiresIn })
// }
// Flash fork: BTC invoices are not supported, but the legacy Galoy tests still
// reference this API. Keep the export typed so the test suite can typecheck.
export const addInvoiceForSelfForBtcWallet = async (
args: AddInvoiceForSelfForBtcWalletArgs,
): Promise<LnInvoice | ApplicationError> => {
return new UnsupportedCurrencyError(
`BTC invoices are not supported for wallet ${args.walletId}`,
)
}

export const addInvoiceForSelfForUsdWallet = async (
args: AddInvoiceForSelfForUsdWalletArgs,
Expand Down Expand Up @@ -122,12 +119,11 @@ const addInvoiceForRecipient = async ({
recipientWalletId,
amount,
memo = "",
descriptionHash,
expiresIn,
}: AddInvoiceForRecipientArgs): Promise<LnInvoice | ApplicationError> => {
const wallet = await WalletsRepository().findById(recipientWalletId)
if (wallet instanceof Error) return wallet

const account = await AccountsRepository().findById(wallet.accountId)
if (account instanceof Error) return account

Expand All @@ -143,26 +139,20 @@ const addInvoiceForRecipient = async ({
amount: checkedAmount,
accountId: recipientWalletId,
memo,
expiration: expiresIn ? expiresIn * 60 as Seconds : undefined,
expiration: expiresIn ? ((expiresIn * 60) as Seconds) : undefined,
})
if (resp instanceof IbexError) return resp

return toDomainInvoice(resp)
}

// export const addInvoiceForRecipientForBtcWallet = async (
// args: AddInvoiceForRecipientForBtcWalletArgs,
// ): Promise<LnInvoice | ApplicationError> => {
// const recipientWalletId = checkedToWalletId(args.recipientWalletId)
// if (recipientWalletId instanceof Error) return recipientWalletId

// const expiresIn = checkedToMinutes(args.expiresIn || defaultBtcExpiration)
// if (expiresIn instanceof Error) return expiresIn

// const validated = await validateIsBtcWallet(recipientWalletId)
// if (validated instanceof Error) return validated
// return addInvoiceForRecipient({ ...args, recipientWalletId, expiresIn })
// }
export const addInvoiceForRecipientForBtcWallet = async (
args: AddInvoiceForRecipientForBtcWalletArgs,
): Promise<LnInvoice | ApplicationError> => {
return new UnsupportedCurrencyError(
`BTC invoices are not supported for wallet ${args.recipientWalletId}`,
)
}

export const addInvoiceForRecipientForUsdWallet = async (
args: AddInvoiceForRecipientForUsdWalletArgs,
Expand All @@ -176,7 +166,6 @@ export const addInvoiceForRecipientForUsdWallet = async (
const validated = await validateIsUsdWallet(recipientWalletId)
if (validated instanceof Error) return validated


return addInvoiceForRecipient({ ...args, recipientWalletId, expiresIn })
}

Expand Down Expand Up @@ -273,12 +262,12 @@ const checkRecipientWalletIdRateLimits = async (
})

// Takes a successful Ibex Response and returns a domain 'LnInvoice' or error
const toDomainInvoice = (ibex: AddInvoiceResponse201): (LnInvoice | ApplicationError) => {
const toDomainInvoice = (ibex: AddInvoiceResponse201): LnInvoice | ApplicationError => {
const invoiceString: string | undefined = ibex.invoice?.bolt11
if (!invoiceString) return new UnexpectedIbexResponse("Could not find invoice.")

const decodedInvoice = decodeInvoice(invoiceString)
if (decodedInvoice instanceof Error) return decodedInvoice
if (decodedInvoice instanceof Error) return decodedInvoice

return {
destination: decodedInvoice.destination,
Expand All @@ -295,4 +284,4 @@ const toDomainInvoice = (ibex: AddInvoiceResponse201): (LnInvoice | ApplicationE
expiresAt: decodedInvoice.expiresAt,
isExpired: decodedInvoice.isExpired,
}
}
}
16 changes: 8 additions & 8 deletions src/app/wallets/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ type AddInvoiceForSelfArgs = {
expiresIn: Minutes
}

// type AddInvoiceForSelfForBtcWalletArgs = {
// walletId: string
// amount: number
// memo?: string
// expiresIn?: number
// }
type AddInvoiceForSelfForBtcWalletArgs = {
walletId: string
amount: BtcPaymentAmount
memo?: string
expiresIn?: number
}

type AddInvoiceForSelfForUsdWalletArgs = {
walletId: string
Expand All @@ -35,7 +35,7 @@ type AddInvoiceForRecipientArgs = {

type AddInvoiceForRecipientForBtcWalletArgs = {
recipientWalletId: string
amount: number
amount: BtcPaymentAmount
memo?: string
descriptionHash?: string
expiresIn?: number
Expand Down Expand Up @@ -72,7 +72,7 @@ type GetOnChainFeeWithoutCurrencyArgs = {
walletId: WalletId
account: Account
amount: number
address: OnChainAddress
address: string | OnChainAddress
speed: PayoutSpeed
}

Expand Down
58 changes: 34 additions & 24 deletions src/app/wallets/send-on-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import { UnsupportedCurrencyError } from "@domain/errors"
import { ErrorLevel, USDAmount } from "@domain/shared"
import { OnchainUsdPaymentValidator } from "@domain/wallets"

import {
AccountsRepository,
WalletsRepository,
} from "@services/mongoose"
import { AccountsRepository, WalletsRepository } from "@services/mongoose"
import { recordExceptionInCurrentSpan } from "@services/tracing"

import Ibex from "@services/ibex/client"
Expand All @@ -22,37 +19,40 @@ type PayOnChainByWalletIdWithoutCurrencyArgs = {
address: string
speed: PayoutSpeed
memo: string | null
amount?: number | FractionalCentAmount | USDAmount
}

type PayOnChainByUsdArgs = PayOnChainByWalletIdWithoutCurrencyArgs & {
amount: USDAmount
}

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

/*
* The following code has been changed for Flash as follows:
*
* 1. Intraledger payments -
* Ibex does not allow us to check if address is Ibex owned,
* so we are currently unable to distinguish intraledger vs external
* To do so, we would need to track the address ourselves in mongo
* 2. Rate conversion (BTC-USD) - Ibex handles
* 3. Volume limit checks - either need to reference an internal ledger or,
* get recent trx volume from Ibex
*
* To reintroduce, see the Galoy codebase:
*/
export const payOnChainByWalletId = async <R extends WalletCurrency>({
* The following code has been changed for Flash as follows:
*
* 1. Intraledger payments -
* Ibex does not allow us to check if address is Ibex owned,
* so we are currently unable to distinguish intraledger vs external
* To do so, we would need to track the address ourselves in mongo
* 2. Rate conversion (BTC-USD) - Ibex handles
* 3. Volume limit checks - either need to reference an internal ledger or,
* get recent trx volume from Ibex
*
* To reintroduce, see the Galoy codebase:
*/
export const payOnChainByWalletId = async ({
senderAccount,
senderWalletId,
amount,
address,
speed,
memo,
}: PayOnChainByUsdArgs): Promise<PayOnChainByWalletIdResult | Error> => {
// For testing purposes, would be nice to extract these functions
const latestAccountState = await AccountsRepository().findById(senderAccount.id)
if (latestAccountState instanceof Error) return latestAccountState

const senderWallet = await WalletsRepository().findById(senderWalletId as WalletId)
if (senderWallet instanceof Error) return senderWallet

Expand All @@ -77,13 +77,13 @@ export const payOnChainByWalletId = async <R extends WalletCurrency>({

const resp = await Ibex.sendOnchain(args)
if (resp instanceof IbexError) return resp

let status = IbexAdaptor.toPaymentSendStatus(resp.status)
if (status instanceof UnexpectedIbexResponse) {
recordExceptionInCurrentSpan({
error: status,
level: ErrorLevel.Warn,
})
})
status = PaymentSendStatus.Pending
}
return {
Expand All @@ -96,11 +96,21 @@ export const payOnChainByWalletId = async <R extends WalletCurrency>({
export const payOnChainByWalletIdForBtcWallet = async (
args: PayOnChainByWalletIdWithoutCurrencyArgs,
): Promise<PayOnChainByWalletIdResult | ApplicationError> => {
return new UnsupportedCurrencyError("BTC amount is not supported")
return new UnsupportedCurrencyError(
`BTC amount is not supported for wallet ${args.senderWalletId}`,
)
}

export const payOnChainByWalletIdForUsdWallet = async (
args: PayOnChainByWalletIdForUsdWalletArgs,
): Promise<PayOnChainByWalletIdResult | ApplicationError> => {
return payOnChainByWalletId(args as PayOnChainByUsdArgs)
}

export const payOnChainByWalletIdForUsdWalletAndBtcAmount = async (
args: PayOnChainByWalletIdWithoutCurrencyArgs,
): Promise<PayOnChainByWalletIdResult | ApplicationError> => {
return new UnsupportedCurrencyError("BTC amount is not supported")
return new UnsupportedCurrencyError(
`BTC amount is not supported for wallet ${args.senderWalletId}`,
)
}
25 changes: 7 additions & 18 deletions src/domain/wallets/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type BaseWalletTransaction = {
readonly settlementAmount: Satoshis | UsdCents
readonly settlementFee: Satoshis | UsdCents
readonly settlementCurrency: WalletCurrency

readonly settlementDisplayAmount: DisplayCurrencyMajorAmount
readonly settlementDisplayFee: DisplayCurrencyMajorAmount
readonly settlementDisplayPrice: WalletMinorUnitDisplayPrice<
Expand Down Expand Up @@ -131,12 +131,12 @@ type WalletLnSettledTransaction = BaseWalletTransaction & {
}

type UnknownTypeTransaction = BaseWalletTransaction & {
readonly initiationVia: {
readonly type: 'unknown'
}
readonly settlementVia: {
readonly type: 'unknown'
}
readonly initiationVia: {
readonly type: "unknown"
}
readonly settlementVia: {
readonly type: "unknown"
}
}

type WalletOnChainTransaction =
Expand Down Expand Up @@ -235,14 +235,3 @@ type OnChainFeeCalculator = {
}
intraLedgerFees(): PaymentAmountInAllCurrencies
}

type PaymentInputValidatorConfig = (
walletId: WalletId,
) => Promise<Wallet | RepositoryError>


type PaymentInputValidator = {
validatePaymentInput: <T extends undefined | string>(
args: ValidatePaymentInputArgs<T>,
) => Promise<ValidatePaymentInputRet<T> | ValidationError | RepositoryError>
}
18 changes: 6 additions & 12 deletions src/domain/wallets/payment-input-validator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
BtcAmount,
USDAmount,
ValidationError,
isUsdWallet,
validator,
} from "@domain/shared"
import { USDAmount, ValidationError, isUsdWallet, validator } from "@domain/shared"
import { isActiveAccount, walletBelongsToAccount } from "@domain/accounts"
import { SendOnchainArgs } from "@services/ibex/types"

Expand All @@ -17,21 +11,21 @@ import { SendOnchainArgs } from "@services/ibex/types"
const checkOnchainMin = async (o: { amount: USDAmount }) => {
// TODO: Currently relying on Ibex to enforce dust limits
// const { dustThreshold } = getOnChainWalletConfig()
// const minBtc = BtcAmount.sats(dustThreshold.toString())
// 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
return o.amount.isGreaterThan(minUsd)
? true
: new ValidationError(`Amount must be greater than ${minUsd.asDollars()}`)
}

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

export const OnchainUsdPaymentValidator = validator<SendOnchainArgsWithContext>([
isUsdWallet,
isActiveAccount,
walletBelongsToAccount,
checkOnchainMin,
])
])
Loading