Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: complete PayoutCancelled handler implementation #3418

Merged
merged 3 commits into from
Oct 25, 2023
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
54 changes: 40 additions & 14 deletions core/api/src/servers/event-handlers/bria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,42 @@ import {
CouldNotFindWalletFromOnChainAddressError,
LessThanDustThresholdError,
NoTransactionToUpdateError,
NotImplementedError,
} from "@/domain/errors"

import { NoTransactionToSettleError } from "@/services/ledger/domain/errors"
import * as LedgerFacade from "@/services/ledger/facade"
import { baseLogger } from "@/services/logger"
import { BriaPayloadType } from "@/services/bria"
import { EventAugmentationMissingError } from "@/services/bria/errors"
import {
EventAugmentationMissingError,
UnknownPayloadTypeReceivedError,
} from "@/services/bria/errors"
import { addAttributesToCurrentSpan } from "@/services/tracing"

// This should never compile if 'payloadType' is not never
const assertUnreachable = (payloadType: never): Error =>
new UnknownPayloadTypeReceivedError(payloadType)

const isBriaPayoutEvent = (payload: BriaPayload): payload is BriaPayoutPayload => {
return (payload as BriaPayoutPayload).id !== undefined
}

export const briaEventHandler = async (event: BriaEvent): Promise<true | DomainError> => {
baseLogger.info(
{ sequence: event.sequence, type: event.payload.type },
{
sequence: event.sequence,
type: event.payload.type,
...(isBriaPayoutEvent(event.payload) ? { id: event.payload.id } : {}),
},
"bria event handler",
)
addAttributesToCurrentSpan({
["event.sequence"]: event.sequence,
["event.type"]: event.payload.type,
})

switch (event.payload.type) {
const payloadType = event.payload.type
switch (payloadType) {
case BriaPayloadType.UtxoDetected:
return utxoDetectedEventHandler({
event: event.payload,
Expand All @@ -46,8 +61,17 @@ export const briaEventHandler = async (event: BriaEvent): Promise<true | DomainE
payoutInfo: event.augmentation.payoutInfo,
})

case BriaPayloadType.PayoutCommitted:
return true

case BriaPayloadType.PayoutCancelled:
return payoutCancelledEventHandler({ event: event.payload })
if (event.augmentation.payoutInfo === undefined) {
return new EventAugmentationMissingError()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cant block event processor because of this... stablesats didnt use externalId, also we should stop using === undefined (payout info can be null)

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should never be null when passing augment option

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also don't understand relevance of stablesats didnt use externalId? Since rollout on Monday it is actually using externalId - but what does that have to do with anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cant block event processor because of this... stablesats didnt use externalId, also we should stop using === undefined (payout info can be null)

payoutInfo can't be null in this scenario because we always pass the augmentation. In the instances where an externalId isn't explicitly passed to stablesats (before Monday) a random uuid is internally assigned. Can see the difference in the two clusters below.

Traces: https://ui.honeycomb.io/galoy/datasets/galoy-bbw/result/qEnUKXWF6pf

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also don't understand relevance of stablesats didnt use externalId? Since rollout on Monday it is actually using externalId - but what does that have to do with anything?

we need to reprocess txs

}
return payoutCancelledEventHandler({
event: event.payload,
payoutInfo: event.augmentation.payoutInfo,
})

case BriaPayloadType.PayoutBroadcast:
if (event.augmentation.payoutInfo === undefined) {
Expand All @@ -62,7 +86,7 @@ export const briaEventHandler = async (event: BriaEvent): Promise<true | DomainE
return payoutSettledEventHandler({ event: event.payload })

default:
return true
return assertUnreachable(payloadType)
}
}

Expand Down Expand Up @@ -137,18 +161,20 @@ export const payoutSubmittedEventHandler = async ({

export const payoutCancelledEventHandler = async ({
event,
payoutInfo,
}: {
event: PayoutCancelled
payoutInfo: PayoutAugmentation
}): Promise<true | ApplicationError> => {
const txns = await LedgerFacade.getTransactionsByPayoutId(event.id)
if (txns instanceof Error) return txns

if (txns.length !== 0)
return new NotImplementedError(
`Payout cancels not implemented as yet for PayoutId: ${event.id}`,
)
const res = await LedgerFacade.recordOnChainSendRevert({
journalId: payoutInfo.externalId as LedgerJournalId,
payoutId: event.id,
})
if (res instanceof NoTransactionToUpdateError) {
return true
}

return true
return res
}

export const payoutBroadcastEventHandler = async ({
Expand Down
7 changes: 3 additions & 4 deletions core/api/src/services/bria/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ export const BriaPayloadType = {

const eventRepo = BriaEventRepo()

const assertUnreachable = (payloadCase: never): Error => {
// This should never compile if 'payloadCase' is not never
return new UnknownPayloadTypeReceivedError(payloadCase)
}
// This should never compile if 'payloadCase' is not never
const assertUnreachable = (payloadCase: never): Error =>
new UnknownPayloadTypeReceivedError(payloadCase)

export const eventDataHandler =
(eventHandler: BriaEventHandler) =>
Expand Down
7 changes: 3 additions & 4 deletions core/api/src/services/bria/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,15 @@ type PayoutCancelled = {
address: OnChainAddress
}

type BriaPayload =
| UtxoDetected
| UtxoDropped
| UtxoSettled
type BriaPayoutPayload =
| PayoutSubmitted
| PayoutCommitted
| PayoutBroadcast
| PayoutSettled
| PayoutCancelled

type BriaPayload = UtxoDetected | UtxoDropped | UtxoSettled | BriaPayoutPayload

type BriaEvent = {
payload: BriaPayload
augmentation: BriaEventAugmentation
Expand Down
33 changes: 32 additions & 1 deletion core/api/src/services/ledger/facade/onchain-send.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { JournalNotFoundError } from "medici"

import { MainBook, Transaction } from "../books"

import { getBankOwnerWalletId, getNonEndUserWalletIds } from "../caching"
Expand All @@ -7,7 +9,8 @@ import {
toLedgerAccountDescriptor,
} from "../domain"
import { NoTransactionToSettleError, UnknownLedgerError } from "../domain/errors"
import { persistAndReturnEntry } from "../helpers"
import { persistAndReturnEntry, translateToLedgerJournal } from "../helpers"
import { TransactionsMetadataRepository } from "../services"

import { translateToLedgerTx } from ".."

Expand Down Expand Up @@ -100,6 +103,34 @@ export const setOnChainTxPayoutId = async ({
}
}

export const recordOnChainSendRevert = async ({
journalId,
payoutId,
}: SetOnChainTxPayoutIdArgs): Promise<true | LedgerServiceError> => {
const reason = "Payment canceled"
try {
if (!isValidObjectId(journalId)) {
return new NoTransactionToUpdateError(JSON.stringify({ journalId }))
}

const savedEntry = await MainBook.void(journalId, reason)

const journalEntry = translateToLedgerJournal(savedEntry)
const txsMetadataToPersist = journalEntry.transactionIds.map((id) => ({
id,
}))
await TransactionsMetadataRepository().persistAll(txsMetadataToPersist)

return true
} catch (err) {
if (err instanceof JournalNotFoundError) {
return new NoTransactionToUpdateError(JSON.stringify({ journalId, payoutId }))
}

return new UnknownLedgerError(err)
}
}

export const setOnChainTxIdByPayoutId = async ({
payoutId,
txId,
Expand Down
67 changes: 67 additions & 0 deletions core/api/test/bats/onchain-send.bats
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,70 @@ teardown() {
amount="$(graphql_output '.data.onChainUsdTxFeeAsBtcDenominated.amount')"
[[ "${amount}" == 0 ]] || exit 1
}

@test "onchain-send: cancel external payout" {
payout_id=$(bria_cli submit-payout \
-w dev-wallet \
-q dev-queue \
-d bc1qxnjv6rqqzxc6kglyasljmwupwrlv5n5uqkyuk0 \
-a 1000000000 \
| jq -r '.id'
)
[[ "${payout_id}" != "null" ]] || exit 1
retry 10 1 grep "sequence.*payout_submitted.*${payout_id}" .e2e-trigger.log

last_sequence=$(
grep "sequence" .e2e-trigger.log \
| tail -n 1 \
| jq -r '.sequence'
)
[[ -n "${last_sequence}" ]] || exit 1

bria_cli cancel-payout -i ${payout_id}
retry 10 1 grep "sequence\":${sequence}.*payout_cancelled.*${payout_id}" .e2e-trigger.log
}

@test "onchain-send: cancel internal payout" {
token_name="$ALICE_TOKEN_NAME"
btc_wallet_name="$token_name.btc_wallet_id"
usd_wallet_name="$token_name.usd_wallet_id"

# Get last sequence
last_sequence=$(
grep "sequence" .e2e-trigger.log \
| tail -n 1 \
| jq -r '.sequence'
)
if [[ -z "${last_sequence}" ]]; then
sequence=1
else
sequence="$(( $last_sequence + 1 ))"
fi

# Initiate internal payout
on_chain_payment_send_address=$(bitcoin_cli getnewaddress)
[[ "${on_chain_payment_send_address}" != "null" ]] || exit 1

variables=$(
jq -n \
--arg wallet_id "$(read_value $btc_wallet_name)" \
--arg address "$on_chain_payment_send_address" \
--arg amount 12345 \
'{input: {walletId: $wallet_id, address: $address, amount: $amount}}'
)
exec_graphql "$token_name" 'on-chain-payment-send' "$variables"
send_status="$(graphql_output '.data.onChainPaymentSend.status')"
[[ "${send_status}" = "SUCCESS" ]] || exit 1

# Parse payout_id value
retry 10 1 grep "sequence\":${sequence}.*payout_submitted" .e2e-trigger.log
payout_id=$(
grep "sequence.*payout_submitted" .e2e-trigger.log \
| tail -n 1 \
| jq -r '.id'
)

# Check for cancelled event
bria_cli cancel-payout -i ${payout_id}
retry 10 1 grep "sequence.*payout_cancelled.*${payout_id}" .e2e-trigger.log
}
Loading