wallet-core: raw/effective amount for push transactions, fix transactions list for push/pull credit

This commit is contained in:
Florian Dold 2023-02-20 03:22:43 +01:00
parent c8b93a37ba
commit d4fda1eea8
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 265 additions and 83 deletions

View File

@ -2084,9 +2084,14 @@ export interface PreparePeerPullDebitRequest {
talerUri: string;
}
export interface CheckPeerPushPaymentResponse {
export interface PreparePeerPushCreditResponse {
contractTerms: PeerContractTerms;
/**
* @deprecated
*/
amount: AmountString;
amountRaw: AmountString;
amountEffective: AmountString;
peerPushPaymentIncomingId: string;
}

View File

@ -1850,6 +1850,8 @@ export interface PeerPushPaymentIncomingRecord {
timestamp: TalerProtocolTimestamp;
estimatedAmountEffective: AmountString;
/**
* Hash of the contract terms. Also
* used to look up the contract terms in the DB.
@ -1865,6 +1867,14 @@ export interface PeerPushPaymentIncomingRecord {
* Associated withdrawal group.
*/
withdrawalGroupId: string | undefined;
/**
* Currency of the peer push payment credit transaction.
*
* Mandatory in current schema version, optional for compatibility
* with older (ver_minor<4) DB versions.
*/
currency: string | undefined;
}
export enum PeerPullPaymentIncomingStatus {
@ -2567,6 +2577,25 @@ export const walletDbFixups: FixupDescription[] = [
});
},
},
{
name: "PeerPushPaymentIncomingRecord_totalCostEstimated_add",
async fn(tx): Promise<void> {
await tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => {
if (pi.estimatedAmountEffective) {
return;
}
const contractTerms = await tx.contractTerms.get(pi.contractTermsHash);
if (!contractTerms) {
// Not sure what we can do here!
} else {
// Not really the cost, but a good substitute for older transactions
// that don't sture the effective cost of the transaction.
pi.estimatedAmountEffective = contractTerms.contractTermsRaw.amount;
await tx.peerPushPaymentIncoming.put(pi);
}
});
},
},
];
const logger = new Logger("db.ts");

View File

@ -31,7 +31,7 @@ import {
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
PreparePeerPushCredit,
CheckPeerPushPaymentResponse,
PreparePeerPushCreditResponse,
Codec,
codecForAmountString,
codecForAny,
@ -100,7 +100,10 @@ import {
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { getTotalRefreshCost } from "./refresh.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
import {
getExchangeWithdrawalInfo,
internalCreateWithdrawalGroup,
} from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
@ -623,7 +626,7 @@ export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
export async function preparePeerPushCredit(
ws: InternalWalletState,
req: PreparePeerPushCredit,
): Promise<CheckPeerPushPaymentResponse> {
): Promise<PreparePeerPushCreditResponse> {
const uri = parsePayPushUri(req.talerUri);
if (!uri) {
@ -658,6 +661,8 @@ export async function preparePeerPushCredit(
if (existing) {
return {
amount: existing.existingContractTerms.amount,
amountEffective: existing.existingPushInc.estimatedAmountEffective,
amountRaw: existing.existingContractTerms.amount,
contractTerms: existing.existingContractTerms,
peerPushPaymentIncomingId:
existing.existingPushInc.peerPushPaymentIncomingId,
@ -705,6 +710,13 @@ export async function preparePeerPushCredit(
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const wi = await getExchangeWithdrawalInfo(
ws,
exchangeBaseUrl,
Amounts.parseOrThrow(purseStatus.balance),
undefined,
);
await ws.db
.mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
.runReadWrite(async (tx) => {
@ -718,6 +730,10 @@ export async function preparePeerPushCredit(
contractTermsHash,
status: PeerPushPaymentIncomingStatus.Proposed,
withdrawalGroupId,
currency: Amounts.currencyOf(purseStatus.balance),
estimatedAmountEffective: Amounts.stringify(
wi.withdrawalAmountEffective,
),
});
await tx.contractTerms.put({
@ -728,6 +744,8 @@ export async function preparePeerPushCredit(
return {
amount: purseStatus.balance,
amountEffective: wi.withdrawalAmountEffective,
amountRaw: purseStatus.balance,
contractTerms: dec.contractTerms,
peerPushPaymentIncomingId,
};

View File

@ -58,6 +58,9 @@ import {
WithdrawalGroupStatus,
RefreshGroupRecord,
RefreshOperationStatus,
PeerPushPaymentIncomingRecord,
PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationRecord,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
@ -135,8 +138,7 @@ export async function getTransactionById(
const { type, args: rest } = parseId("txn", req.transactionId);
if (
type === TransactionType.Withdrawal ||
type === TransactionType.PeerPullCredit ||
type === TransactionType.PeerPushCredit
type === TransactionType.PeerPullCredit
) {
const withdrawalGroupId = rest[0];
return await ws.db
@ -165,24 +167,6 @@ export async function getTransactionById(
ort,
);
}
if (
withdrawalGroupRecord.wgInfo.withdrawalType ===
WithdrawalRecordType.PeerPullCredit
) {
return buildTransactionForPullPaymentCredit(
withdrawalGroupRecord,
ort,
);
}
if (
withdrawalGroupRecord.wgInfo.withdrawalType ===
WithdrawalRecordType.PeerPushCredit
) {
return buildTransactionForPushPaymentCredit(
withdrawalGroupRecord,
ort,
);
}
const exchangeDetails = await getExchangeDetails(
tx,
withdrawalGroupRecord.exchangeBaseUrl,
@ -356,9 +340,15 @@ export async function getTransactionById(
checkDbInvariant(!!ct);
return buildTransactionForPushPaymentDebit(debit, ct.contractTermsRaw);
});
} else if (type === TransactionType.PeerPushCredit) {
// FIXME: Implement!
throw Error("getTransaction not yet implemented for PeerPushCredit");
} else if (type === TransactionType.PeerPushCredit) {
// FIXME: Implement!
throw Error("getTransaction not yet implemented for PeerPullCredit");
} else {
const unknownTxType: never = type;
throw Error(`can't delete a '${unknownTxType}' transaction`);
throw Error(`can't retrieve a '${unknownTxType}' transaction`);
}
}
@ -422,82 +412,144 @@ function buildTransactionForPullPaymentDebit(
};
}
function buildTransactionForPullPaymentCredit(
wsr: WithdrawalGroupRecord,
ort?: OperationRetryRecord,
function buildTransactionForPeerPullCredit(
pullCredit: PeerPullPaymentInitiationRecord,
pullCreditOrt: OperationRetryRecord | undefined,
peerContractTerms: PeerContractTerms,
wsr: WithdrawalGroupRecord | undefined,
wsrOrt: OperationRetryRecord | undefined,
): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
if (wsr) {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
}
/**
* FIXME: this should be handled in the withdrawal process.
* PeerPull withdrawal fails until reserve have funds but it is not
* an error from the user perspective.
*/
const silentWithdrawalErrorForInvoice =
wsrOrt?.lastError &&
wsrOrt.lastError.code ===
TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
return (
e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
e.httpStatusCode === 409
);
});
return {
type: TransactionType.PeerPullCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
extendedStatus: wsr.timestampFinish
? ExtendedStatus.Done
: ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: pullCredit.mergeTimestamp,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
},
talerUri: constructPayPullUri({
exchangeBaseUrl: wsr.exchangeBaseUrl,
contractPriv: wsr.wgInfo.contractPriv,
}),
transactionId: makeTransactionId(
TransactionType.PeerPullCredit,
pullCredit.pursePub,
),
frozen: false,
...(wsrOrt?.lastError
? {
error: silentWithdrawalErrorForInvoice
? undefined
: wsrOrt.lastError,
}
: {}),
};
}
/**
* FIXME: this should be handled in the withdrawal process.
* PeerPull withdrawal fails until reserve have funds but it is not
* an error from the user perspective.
*/
const silentWithdrawalErrorForInvoice =
ort?.lastError &&
ort.lastError.code === TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
Object.values(ort.lastError.errorsPerCoin ?? {}).every((e) => {
return (
e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
e.httpStatusCode === 409
);
});
return {
type: TransactionType.PeerPullCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
extendedStatus: wsr.timestampFinish
? ExtendedStatus.Done
: ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
amountEffective: Amounts.stringify(peerContractTerms.amount),
amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
extendedStatus: ExtendedStatus.Pending,
pending: true,
timestamp: pullCredit.mergeTimestamp,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
},
talerUri: constructPayPullUri({
exchangeBaseUrl: wsr.exchangeBaseUrl,
contractPriv: wsr.wgInfo.contractPriv,
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
contractPriv: pullCredit.contractPriv,
}),
transactionId: makeTransactionId(
TransactionType.PeerPullCredit,
wsr.withdrawalGroupId,
pullCredit.pursePub,
),
frozen: false,
...(ort?.lastError
? { error: silentWithdrawalErrorForInvoice ? undefined : ort.lastError }
: {}),
...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
};
}
function buildTransactionForPushPaymentCredit(
wsr: WithdrawalGroupRecord,
ort?: OperationRetryRecord,
function buildTransactionForPeerPushCredit(
pushInc: PeerPushPaymentIncomingRecord,
pushOrt: OperationRetryRecord | undefined,
peerContractTerms: PeerContractTerms,
wsr: WithdrawalGroupRecord | undefined,
wsrOrt: OperationRetryRecord | undefined,
): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit)
throw Error("");
if (wsr) {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
throw Error("invalid withdrawal group type for push payment credit");
}
return {
type: TransactionType.PeerPushCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
},
extendedStatus: wsr.timestampFinish
? ExtendedStatus.Done
: ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeTransactionId(
TransactionType.PeerPushCredit,
pushInc.peerPushPaymentIncomingId,
),
frozen: false,
...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}),
};
}
return {
type: TransactionType.PeerPushCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
// FIXME: This is wrong, needs to consider fees!
amountEffective: Amounts.stringify(peerContractTerms.amount),
amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pushInc.exchangeBaseUrl,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
},
extendedStatus: wsr.timestampFinish
? ExtendedStatus.Done
: ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
extendedStatus: ExtendedStatus.Pending,
pending: true,
timestamp: pushInc.timestamp,
transactionId: makeTransactionId(
TransactionType.PeerPushCredit,
wsr.withdrawalGroupId,
pushInc.peerPushPaymentIncomingId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}),
};
}
@ -926,6 +978,8 @@ export async function getTransactions(
x.operationRetries,
x.peerPullPaymentIncoming,
x.peerPushPaymentInitiations,
x.peerPushPaymentIncoming,
x.peerPullPaymentInitiations,
x.planchets,
x.purchases,
x.contractTerms,
@ -970,6 +1024,80 @@ export async function getTransactions(
transactions.push(buildTransactionForPullPaymentDebit(pi));
});
tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => {
if (!pi.currency) {
// Legacy transaction
return;
}
if (shouldSkipCurrency(transactionsRequest, pi.currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
if (pi.status === PeerPushPaymentIncomingStatus.Proposed) {
// We don't report proposed push credit transactions, user needs
// to scan URI again and confirm to see it.
return;
}
const ct = await tx.contractTerms.get(pi.contractTermsHash);
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
if (pi.withdrawalGroupId) {
wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
if (wg) {
const withdrawalOpId = RetryTags.forWithdrawal(wg);
wgOrt = await tx.operationRetries.get(withdrawalOpId);
}
}
const pushIncOpId = RetryTags.forPeerPushCredit(pi);
let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
checkDbInvariant(!!ct);
transactions.push(
buildTransactionForPeerPushCredit(
pi,
pushIncOrt,
ct.contractTermsRaw,
wg,
wgOrt,
),
);
});
tx.peerPullPaymentInitiations.iter().forEachAsync(async (pi) => {
const currency = Amounts.currencyOf(pi.amount);
if (shouldSkipCurrency(transactionsRequest, currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
const ct = await tx.contractTerms.get(pi.contractTermsHash);
let wg: WithdrawalGroupRecord | undefined = undefined;
let wgOrt: OperationRetryRecord | undefined = undefined;
if (pi.withdrawalGroupId) {
wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
if (wg) {
const withdrawalOpId = RetryTags.forWithdrawal(wg);
wgOrt = await tx.operationRetries.get(withdrawalOpId);
}
}
const pushIncOpId = RetryTags.forPeerPullPaymentInitiation(pi);
let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
checkDbInvariant(!!ct);
transactions.push(
buildTransactionForPeerPullCredit(
pi,
pushIncOrt,
ct.contractTermsRaw,
wg,
wgOrt,
),
);
});
tx.refreshGroups.iter().forEachAsync(async (rg) => {
if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
return;
@ -1009,10 +1137,12 @@ export async function getTransactions(
switch (wsr.wgInfo.withdrawalType) {
case WithdrawalRecordType.PeerPullCredit:
transactions.push(buildTransactionForPullPaymentCredit(wsr, ort));
// Will be reported by the corresponding p2p transaction.
// FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
return;
case WithdrawalRecordType.PeerPushCredit:
transactions.push(buildTransactionForPushPaymentCredit(wsr, ort));
// Will be reported by the corresponding p2p transaction.
// FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
return;
case WithdrawalRecordType.BankIntegrated:
transactions.push(

View File

@ -220,12 +220,12 @@ export namespace RetryTags {
export function forPeerPullPaymentDebit(
ppi: PeerPullPaymentIncomingRecord,
): string {
return `${PendingTaskType.PeerPullDebit}:${ppi.pursePub}`;
return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}`;
}
export function forPeerPushCredit(
ppi: PeerPushPaymentIncomingRecord,
): string {
return `${PendingTaskType.PeerPushCredit}:${ppi.pursePub}`;
return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}`;
}
export function byPaymentProposalId(proposalId: string): string {
return `${PendingTaskType.Purchase}:${proposalId}`;

View File

@ -45,7 +45,7 @@ import {
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
PreparePeerPushCredit,
CheckPeerPushPaymentResponse,
PreparePeerPushCreditResponse,
CoinDumpJson,
ConfirmPayRequest,
ConfirmPayResult,
@ -615,7 +615,7 @@ export type InitiatePeerPushDebitOp = {
export type PreparePeerPushCreditOp = {
op: WalletApiOperation.PreparePeerPushCredit;
request: PreparePeerPushCredit;
response: CheckPeerPushPaymentResponse;
response: PreparePeerPushCreditResponse;
};
/**