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; talerUri: string;
} }
export interface CheckPeerPushPaymentResponse { export interface PreparePeerPushCreditResponse {
contractTerms: PeerContractTerms; contractTerms: PeerContractTerms;
/**
* @deprecated
*/
amount: AmountString; amount: AmountString;
amountRaw: AmountString;
amountEffective: AmountString;
peerPushPaymentIncomingId: string; peerPushPaymentIncomingId: string;
} }

View File

@ -1850,6 +1850,8 @@ export interface PeerPushPaymentIncomingRecord {
timestamp: TalerProtocolTimestamp; timestamp: TalerProtocolTimestamp;
estimatedAmountEffective: AmountString;
/** /**
* Hash of the contract terms. Also * Hash of the contract terms. Also
* used to look up the contract terms in the DB. * used to look up the contract terms in the DB.
@ -1865,6 +1867,14 @@ export interface PeerPushPaymentIncomingRecord {
* Associated withdrawal group. * Associated withdrawal group.
*/ */
withdrawalGroupId: string | undefined; 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 { 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"); const logger = new Logger("db.ts");

View File

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

View File

@ -58,6 +58,9 @@ import {
WithdrawalGroupStatus, WithdrawalGroupStatus,
RefreshGroupRecord, RefreshGroupRecord,
RefreshOperationStatus, RefreshOperationStatus,
PeerPushPaymentIncomingRecord,
PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationRecord,
} from "../db.js"; } from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
@ -135,8 +138,7 @@ export async function getTransactionById(
const { type, args: rest } = parseId("txn", req.transactionId); const { type, args: rest } = parseId("txn", req.transactionId);
if ( if (
type === TransactionType.Withdrawal || type === TransactionType.Withdrawal ||
type === TransactionType.PeerPullCredit || type === TransactionType.PeerPullCredit
type === TransactionType.PeerPushCredit
) { ) {
const withdrawalGroupId = rest[0]; const withdrawalGroupId = rest[0];
return await ws.db return await ws.db
@ -165,24 +167,6 @@ export async function getTransactionById(
ort, 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( const exchangeDetails = await getExchangeDetails(
tx, tx,
withdrawalGroupRecord.exchangeBaseUrl, withdrawalGroupRecord.exchangeBaseUrl,
@ -356,9 +340,15 @@ export async function getTransactionById(
checkDbInvariant(!!ct); checkDbInvariant(!!ct);
return buildTransactionForPushPaymentDebit(debit, ct.contractTermsRaw); 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 { } else {
const unknownTxType: never = type; const unknownTxType: never = type;
throw Error(`can't delete a '${unknownTxType}' transaction`); throw Error(`can't retrieve a '${unknownTxType}' transaction`);
} }
} }
@ -422,10 +412,14 @@ function buildTransactionForPullPaymentDebit(
}; };
} }
function buildTransactionForPullPaymentCredit( function buildTransactionForPeerPullCredit(
wsr: WithdrawalGroupRecord, pullCredit: PeerPullPaymentInitiationRecord,
ort?: OperationRetryRecord, pullCreditOrt: OperationRetryRecord | undefined,
peerContractTerms: PeerContractTerms,
wsr: WithdrawalGroupRecord | undefined,
wsrOrt: OperationRetryRecord | undefined,
): Transaction { ): Transaction {
if (wsr) {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) { if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`); throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
} }
@ -435,9 +429,10 @@ function buildTransactionForPullPaymentCredit(
* an error from the user perspective. * an error from the user perspective.
*/ */
const silentWithdrawalErrorForInvoice = const silentWithdrawalErrorForInvoice =
ort?.lastError && wsrOrt?.lastError &&
ort.lastError.code === TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && wsrOrt.lastError.code ===
Object.values(ort.lastError.errorsPerCoin ?? {}).every((e) => { TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
return ( return (
e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
e.httpStatusCode === 409 e.httpStatusCode === 409
@ -452,7 +447,7 @@ function buildTransactionForPullPaymentCredit(
? ExtendedStatus.Done ? ExtendedStatus.Done
: ExtendedStatus.Pending, : ExtendedStatus.Pending,
pending: !wsr.timestampFinish, pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart, timestamp: pullCredit.mergeTimestamp,
info: { info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration, expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary, summary: wsr.wgInfo.contractTerms.summary,
@ -463,21 +458,56 @@ function buildTransactionForPullPaymentCredit(
}), }),
transactionId: makeTransactionId( transactionId: makeTransactionId(
TransactionType.PeerPullCredit, TransactionType.PeerPullCredit,
wsr.withdrawalGroupId, pullCredit.pursePub,
), ),
frozen: false, frozen: false,
...(ort?.lastError ...(wsrOrt?.lastError
? { error: silentWithdrawalErrorForInvoice ? undefined : ort.lastError } ? {
error: silentWithdrawalErrorForInvoice
? undefined
: wsrOrt.lastError,
}
: {}), : {}),
}; };
}
return {
type: TransactionType.PeerPullCredit,
amountEffective: Amounts.stringify(peerContractTerms.amount),
amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
extendedStatus: ExtendedStatus.Pending,
pending: true,
timestamp: pullCredit.mergeTimestamp,
info: {
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
},
talerUri: constructPayPullUri({
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
contractPriv: pullCredit.contractPriv,
}),
transactionId: makeTransactionId(
TransactionType.PeerPullCredit,
pullCredit.pursePub,
),
frozen: false,
...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
};
} }
function buildTransactionForPushPaymentCredit( function buildTransactionForPeerPushCredit(
wsr: WithdrawalGroupRecord, pushInc: PeerPushPaymentIncomingRecord,
ort?: OperationRetryRecord, pushOrt: OperationRetryRecord | undefined,
peerContractTerms: PeerContractTerms,
wsr: WithdrawalGroupRecord | undefined,
wsrOrt: OperationRetryRecord | undefined,
): Transaction { ): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) if (wsr) {
throw Error(""); if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
throw Error("invalid withdrawal group type for push payment credit");
}
return { return {
type: TransactionType.PeerPushCredit, type: TransactionType.PeerPushCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
@ -494,10 +524,32 @@ function buildTransactionForPushPaymentCredit(
timestamp: wsr.timestampStart, timestamp: wsr.timestampStart,
transactionId: makeTransactionId( transactionId: makeTransactionId(
TransactionType.PeerPushCredit, TransactionType.PeerPushCredit,
wsr.withdrawalGroupId, pushInc.peerPushPaymentIncomingId,
), ),
frozen: false, frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}), ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}),
};
}
return {
type: TransactionType.PeerPushCredit,
// FIXME: This is wrong, needs to consider fees!
amountEffective: Amounts.stringify(peerContractTerms.amount),
amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pushInc.exchangeBaseUrl,
info: {
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
},
extendedStatus: ExtendedStatus.Pending,
pending: true,
timestamp: pushInc.timestamp,
transactionId: makeTransactionId(
TransactionType.PeerPushCredit,
pushInc.peerPushPaymentIncomingId,
),
frozen: false,
...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}),
}; };
} }
@ -926,6 +978,8 @@ export async function getTransactions(
x.operationRetries, x.operationRetries,
x.peerPullPaymentIncoming, x.peerPullPaymentIncoming,
x.peerPushPaymentInitiations, x.peerPushPaymentInitiations,
x.peerPushPaymentIncoming,
x.peerPullPaymentInitiations,
x.planchets, x.planchets,
x.purchases, x.purchases,
x.contractTerms, x.contractTerms,
@ -970,6 +1024,80 @@ export async function getTransactions(
transactions.push(buildTransactionForPullPaymentDebit(pi)); 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) => { tx.refreshGroups.iter().forEachAsync(async (rg) => {
if (shouldSkipCurrency(transactionsRequest, rg.currency)) { if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
return; return;
@ -1009,10 +1137,12 @@ export async function getTransactions(
switch (wsr.wgInfo.withdrawalType) { switch (wsr.wgInfo.withdrawalType) {
case WithdrawalRecordType.PeerPullCredit: 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; return;
case WithdrawalRecordType.PeerPushCredit: 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; return;
case WithdrawalRecordType.BankIntegrated: case WithdrawalRecordType.BankIntegrated:
transactions.push( transactions.push(

View File

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

View File

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