wallet-core: p2p support for transactions list

This commit is contained in:
Florian Dold 2022-08-24 22:17:19 +02:00
parent bf516a77e8
commit a11ac57535
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 541 additions and 290 deletions

View File

@ -102,7 +102,11 @@ export type Transaction =
| TransactionRefund | TransactionRefund
| TransactionTip | TransactionTip
| TransactionRefresh | TransactionRefresh
| TransactionDeposit; | TransactionDeposit
| TransactionPeerPullCredit
| TransactionPeerPullDebit
| TransactionPeerPushCredit
| TransactionPeerPushDebit;
export enum TransactionType { export enum TransactionType {
Withdrawal = "withdrawal", Withdrawal = "withdrawal",
@ -111,6 +115,10 @@ export enum TransactionType {
Refresh = "refresh", Refresh = "refresh",
Tip = "tip", Tip = "tip",
Deposit = "deposit", Deposit = "deposit",
PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
PeerPullCredit = "peer-pull-credit",
} }
export enum WithdrawalType { export enum WithdrawalType {
@ -179,6 +187,76 @@ export interface TransactionWithdrawal extends TransactionCommon {
withdrawalDetails: WithdrawalDetails; withdrawalDetails: WithdrawalDetails;
} }
export interface TransactionPeerPullCredit extends TransactionCommon {
type: TransactionType.PeerPullCredit;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
}
export interface TransactionPeerPullDebit extends TransactionCommon {
type: TransactionType.PeerPullDebit;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
amountRaw: AmountString;
amountEffective: AmountString;
}
export interface TransactionPeerPushDebit extends TransactionCommon {
type: TransactionType.PeerPushDebit;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
}
export interface TransactionPeerPushCredit extends TransactionCommon {
type: TransactionType.PeerPushCredit;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
}
export enum PaymentStatus { export enum PaymentStatus {
/** /**
* Explicitly aborted after timeout / failure * Explicitly aborted after timeout / failure
@ -311,10 +389,10 @@ export interface OrderShortInfo {
} }
export interface RefundInfoShort { export interface RefundInfoShort {
transactionId: string, transactionId: string;
timestamp: TalerProtocolTimestamp, timestamp: TalerProtocolTimestamp;
amountEffective: AmountString, amountEffective: AmountString;
amountRaw: AmountString, amountRaw: AmountString;
} }
export interface TransactionRefund extends TransactionCommon { export interface TransactionRefund extends TransactionCommon {

View File

@ -19,7 +19,7 @@
*/ */
import { j2s } from "@gnu-taler/taler-util"; import { j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js"; import { GlobalTestState, WalletCli } from "../harness/harness.js";
import { import {
createSimpleTestkudosEnvironment, createSimpleTestkudosEnvironment,
withdrawViaBank, withdrawViaBank,
@ -31,16 +31,23 @@ import {
export async function runPeerToPeerPullTest(t: GlobalTestState) { export async function runPeerToPeerPullTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { wallet, bank, exchange, merchant } = const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
await createSimpleTestkudosEnvironment(t); t,
);
// Withdraw digital cash into the wallet. // Withdraw digital cash into the wallet.
const wallet1 = new WalletCli(t, "w1");
const wallet2 = new WalletCli(t, "w2");
await withdrawViaBank(t, {
wallet: wallet2,
bank,
exchange,
amount: "TESTKUDOS:20",
});
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); await wallet1.runUntilDone();
await wallet.runUntilDone(); const resp = await wallet1.client.call(
const resp = await wallet.client.call(
WalletApiOperation.InitiatePeerPullPayment, WalletApiOperation.InitiatePeerPullPayment,
{ {
exchangeBaseUrl: exchange.baseUrl, exchangeBaseUrl: exchange.baseUrl,
@ -51,7 +58,7 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
}, },
); );
const checkResp = await wallet.client.call( const checkResp = await wallet2.client.call(
WalletApiOperation.CheckPeerPullPayment, WalletApiOperation.CheckPeerPullPayment,
{ {
talerUri: resp.talerUri, talerUri: resp.talerUri,
@ -60,18 +67,27 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
console.log(`checkResp: ${j2s(checkResp)}`); console.log(`checkResp: ${j2s(checkResp)}`);
const acceptResp = await wallet.client.call( const acceptResp = await wallet2.client.call(
WalletApiOperation.AcceptPeerPullPayment, WalletApiOperation.AcceptPeerPullPayment,
{ {
peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId, peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId,
}, },
); );
const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {}); await wallet1.runUntilDone();
await wallet2.runUntilDone();
console.log(`transactions: ${j2s(txs)}`); const txn1 = await wallet1.client.call(
WalletApiOperation.GetTransactions,
{},
);
const txn2 = await wallet2.client.call(
WalletApiOperation.GetTransactions,
{},
);
await wallet.runUntilDone(); console.log(`txn1: ${j2s(txn1)}`);
console.log(`txn2: ${j2s(txn2)}`);
} }
runPeerToPeerPullTest.suites = ["wallet"]; runPeerToPeerPullTest.suites = ["wallet"];

View File

@ -17,6 +17,7 @@
/** /**
* Imports. * Imports.
*/ */
import { j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { GlobalTestState, WalletCli } from "../harness/harness.js";
import { import {
@ -78,6 +79,18 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
await wallet1.runUntilDone(); await wallet1.runUntilDone();
await wallet2.runUntilDone(); await wallet2.runUntilDone();
const txn1 = await wallet1.client.call(
WalletApiOperation.GetTransactions,
{},
);
const txn2 = await wallet2.client.call(
WalletApiOperation.GetTransactions,
{},
);
console.log(`txn1: ${j2s(txn1)}`);
console.log(`txn2: ${j2s(txn2)}`);
} }
runPeerToPeerPushTest.suites = ["wallet"]; runPeerToPeerPushTest.suites = ["wallet"];

View File

@ -1219,6 +1219,13 @@ export interface DenomSelectionState {
}[]; }[];
} }
export const enum WithdrawalRecordType {
BankManual = "bank-manual",
BankIntegrated = "bank-integrated",
PeerPullCredit = "peer-pull-credit",
PeerPushCredit = "peer-push-credit",
}
/** /**
* Group of withdrawal operations that need to be executed. * Group of withdrawal operations that need to be executed.
* (Either for a normal withdrawal or from a tip.) * (Either for a normal withdrawal or from a tip.)
@ -1232,6 +1239,8 @@ export interface WithdrawalGroupRecord {
*/ */
withdrawalGroupId: string; withdrawalGroupId: string;
withdrawalType: WithdrawalRecordType;
/** /**
* Secret seed used to derive planchets. * Secret seed used to derive planchets.
* Stored since planchets are created lazily. * Stored since planchets are created lazily.
@ -1607,8 +1616,6 @@ export interface PeerPushPaymentInitiationRecord {
contractPriv: string; contractPriv: string;
contractPub: string;
purseExpiration: TalerProtocolTimestamp; purseExpiration: TalerProtocolTimestamp;
/** /**
@ -1681,7 +1688,11 @@ export interface PeerPullPaymentIncomingRecord {
contractTerms: PeerContractTerms; contractTerms: PeerContractTerms;
timestamp: TalerProtocolTimestamp; timestampCreated: TalerProtocolTimestamp;
paid: boolean;
accepted: boolean;
contractPriv: string; contractPriv: string;
} }
@ -1878,9 +1889,18 @@ export const WalletStoresV1 = {
]), ]),
}, },
), ),
peerPullPaymentInitiation: describeStore( peerPullPaymentInitiations: describeStore(
describeContents<PeerPullPaymentInitiationRecord>( describeContents<PeerPullPaymentInitiationRecord>(
"peerPushPaymentInitiation", "peerPullPaymentInitiations",
{
keyPath: "pursePub",
},
),
{},
),
peerPushPaymentInitiations: describeStore(
describeContents<PeerPushPaymentInitiationRecord>(
"peerPushPaymentInitiations",
{ {
keyPath: "pursePub", keyPath: "pursePub",
}, },

View File

@ -65,6 +65,7 @@ import {
MergeReserveInfo, MergeReserveInfo,
ReserveRecordStatus, ReserveRecordStatus,
WalletStoresV1, WalletStoresV1,
WithdrawalRecordType,
} from "../db.js"; } from "../db.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
@ -208,39 +209,6 @@ export async function initiatePeerToPeerPush(
): Promise<InitiatePeerPushPaymentResponse> { ): Promise<InitiatePeerPushPaymentResponse> {
// FIXME: actually create a record for retries here! // FIXME: actually create a record for retries here!
const instructedAmount = Amounts.parseOrThrow(req.amount); const instructedAmount = Amounts.parseOrThrow(req.amount);
const coinSelRes: PeerCoinSelection | undefined = await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
coins: x.coins,
denominations: x.denominations,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const sel = await selectPeerCoins(ws, tx, instructedAmount);
if (!sel) {
return undefined;
}
const pubs: CoinPublicKey[] = [];
for (const c of sel.coins) {
const coin = await tx.coins.get(c.coinPub);
checkDbInvariant(!!coin);
coin.currentAmount = Amounts.sub(
coin.currentAmount,
Amounts.parseOrThrow(c.contribution),
).amount;
await tx.coins.put(coin);
}
await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay);
return sel;
});
logger.info(`selected p2p coins: ${j2s(coinSelRes)}`);
if (!coinSelRes) {
throw Error("insufficient balance");
}
const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({});
@ -260,6 +228,62 @@ export async function initiatePeerToPeerPush(
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
const econtractResp = await ws.cryptoApi.encryptContractForMerge({
contractTerms,
mergePriv: mergePair.priv,
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
});
const coinSelRes: PeerCoinSelection | undefined = await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
coins: x.coins,
denominations: x.denominations,
refreshGroups: x.refreshGroups,
peerPushPaymentInitiations: x.peerPushPaymentInitiations,
}))
.runReadWrite(async (tx) => {
const sel = await selectPeerCoins(ws, tx, instructedAmount);
if (!sel) {
return undefined;
}
const pubs: CoinPublicKey[] = [];
for (const c of sel.coins) {
const coin = await tx.coins.get(c.coinPub);
checkDbInvariant(!!coin);
coin.currentAmount = Amounts.sub(
coin.currentAmount,
Amounts.parseOrThrow(c.contribution),
).amount;
await tx.coins.put(coin);
}
await tx.peerPushPaymentInitiations.add({
amount: Amounts.stringify(instructedAmount),
contractPriv: econtractResp.contractPriv,
exchangeBaseUrl: sel.exchangeBaseUrl,
mergePriv: mergePair.priv,
mergePub: mergePair.pub,
// FIXME: only set this later!
purseCreated: true,
purseExpiration: purseExpiration,
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
timestampCreated: TalerProtocolTimestamp.now(),
});
await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay);
return sel;
});
logger.info(`selected p2p coins: ${j2s(coinSelRes)}`);
if (!coinSelRes) {
throw Error("insufficient balance");
}
const purseSigResp = await ws.cryptoApi.signPurseCreation({ const purseSigResp = await ws.cryptoApi.signPurseCreation({
hContractTerms, hContractTerms,
mergePub: mergePair.pub, mergePub: mergePair.pub,
@ -280,13 +304,6 @@ export async function initiatePeerToPeerPush(
coinSelRes.exchangeBaseUrl, coinSelRes.exchangeBaseUrl,
); );
const econtractResp = await ws.cryptoApi.encryptContractForMerge({
contractTerms,
mergePriv: mergePair.priv,
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
});
const httpResp = await ws.http.postJson(createPurseUrl.href, { const httpResp = await ws.http.postJson(createPurseUrl.href, {
amount: Amounts.stringify(instructedAmount), amount: Amounts.stringify(instructedAmount),
merge_pub: mergePair.pub, merge_pub: mergePair.pub,
@ -517,6 +534,7 @@ export async function acceptPeerPushPayment(
await internalCreateWithdrawalGroup(ws, { await internalCreateWithdrawalGroup(ws, {
amount, amount,
withdrawalType: WithdrawalRecordType.PeerPushCredit,
exchangeBaseUrl: peerInc.exchangeBaseUrl, exchangeBaseUrl: peerInc.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus, reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveKeyPair: { reserveKeyPair: {
@ -554,6 +572,7 @@ export async function acceptPeerPullPayment(
coins: x.coins, coins: x.coins,
denominations: x.denominations, denominations: x.denominations,
refreshGroups: x.refreshGroups, refreshGroups: x.refreshGroups,
peerPullPaymentIncoming: x.peerPullPaymentIncoming,
})) }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const sel = await selectPeerCoins(ws, tx, instructedAmount); const sel = await selectPeerCoins(ws, tx, instructedAmount);
@ -574,6 +593,15 @@ export async function acceptPeerPullPayment(
await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay); await createRefreshGroup(ws, tx, pubs, RefreshReason.Pay);
const pi = await tx.peerPullPaymentIncoming.get(
req.peerPullPaymentIncomingId,
);
if (!pi) {
throw Error();
}
pi.accepted = true;
await tx.peerPullPaymentIncoming.put(pi);
return sel; return sel;
}); });
logger.info(`selected p2p coins: ${j2s(coinSelRes)}`); logger.info(`selected p2p coins: ${j2s(coinSelRes)}`);
@ -656,8 +684,10 @@ export async function checkPeerPullPayment(
contractPriv: contractPriv, contractPriv: contractPriv,
exchangeBaseUrl: exchangeBaseUrl, exchangeBaseUrl: exchangeBaseUrl,
pursePub: pursePub, pursePub: pursePub,
timestamp: TalerProtocolTimestamp.now(), timestampCreated: TalerProtocolTimestamp.now(),
contractTerms: dec.contractTerms, contractTerms: dec.contractTerms,
paid: false,
accepted: false,
}); });
}); });
@ -672,6 +702,8 @@ export async function initiatePeerRequestForPay(
ws: InternalWalletState, ws: InternalWalletState,
req: InitiatePeerPullPaymentRequest, req: InitiatePeerPullPaymentRequest,
): Promise<InitiatePeerPullPaymentResponse> { ): Promise<InitiatePeerPullPaymentResponse> {
await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
const mergeReserveInfo = await getMergeReserveInfo(ws, { const mergeReserveInfo = await getMergeReserveInfo(ws, {
exchangeBaseUrl: req.exchangeBaseUrl, exchangeBaseUrl: req.exchangeBaseUrl,
}); });
@ -727,7 +759,7 @@ export async function initiatePeerRequestForPay(
await ws.db await ws.db
.mktx((x) => ({ .mktx((x) => ({
peerPullPaymentInitiation: x.peerPullPaymentInitiation, peerPullPaymentInitiation: x.peerPullPaymentInitiations,
})) }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.peerPullPaymentInitiation.put({ await tx.peerPullPaymentInitiation.put({
@ -772,6 +804,7 @@ export async function initiatePeerRequestForPay(
await internalCreateWithdrawalGroup(ws, { await internalCreateWithdrawalGroup(ws, {
amount: Amounts.parseOrThrow(req.amount), amount: Amounts.parseOrThrow(req.amount),
withdrawalType: WithdrawalRecordType.PeerPullCredit,
exchangeBaseUrl: req.exchangeBaseUrl, exchangeBaseUrl: req.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus, reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveKeyPair: { reserveKeyPair: {

View File

@ -38,6 +38,7 @@ import {
RefundState, RefundState,
ReserveRecordStatus, ReserveRecordStatus,
WalletRefundItem, WalletRefundItem,
WithdrawalRecordType,
} from "../db.js"; } from "../db.js";
import { processDepositGroup } from "./deposits.js"; import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
@ -101,10 +102,14 @@ const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Withdrawal]: 1, [TransactionType.Withdrawal]: 1,
[TransactionType.Tip]: 2, [TransactionType.Tip]: 2,
[TransactionType.Payment]: 3, [TransactionType.Payment]: 3,
[TransactionType.Refund]: 4, [TransactionType.PeerPullCredit]: 4,
[TransactionType.Deposit]: 5, [TransactionType.PeerPullDebit]: 5,
[TransactionType.Refresh]: 6, [TransactionType.PeerPushCredit]: 6,
[TransactionType.Tip]: 7, [TransactionType.PeerPushDebit]: 7,
[TransactionType.Refund]: 8,
[TransactionType.Deposit]: 9,
[TransactionType.Refresh]: 10,
[TransactionType.Tip]: 11,
}; };
/** /**
@ -131,267 +136,348 @@ export async function getTransactions(
recoupGroups: x.recoupGroups, recoupGroups: x.recoupGroups,
depositGroups: x.depositGroups, depositGroups: x.depositGroups,
tombstones: x.tombstones, tombstones: x.tombstones,
peerPushPaymentInitiations: x.peerPushPaymentInitiations,
peerPullPaymentIncoming: x.peerPullPaymentIncoming,
})) }))
.runReadOnly( .runReadOnly(async (tx) => {
// Report withdrawals that are currently in progress. tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
async (tx) => { const amount = Amounts.parseOrThrow(pi.amount);
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
if ( return;
shouldSkipCurrency( }
transactionsRequest, if (shouldSkipSearch(transactionsRequest, [])) {
wsr.rawWithdrawalAmount.currency, return;
) }
) { transactions.push({
return; type: TransactionType.PeerPushDebit,
} amountEffective: pi.amount,
amountRaw: pi.amount,
exchangeBaseUrl: pi.exchangeBaseUrl,
frozen: false,
pending: !pi.purseCreated,
timestamp: pi.timestampCreated,
transactionId: makeEventId(
TransactionType.PeerPushDebit,
pi.pursePub,
),
});
});
if (shouldSkipSearch(transactionsRequest, [])) { tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
return; const amount = Amounts.parseOrThrow(pi.contractTerms.amount);
} if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
let withdrawalDetails: WithdrawalDetails; return;
if (wsr.bankInfo) { }
withdrawalDetails = { if (shouldSkipSearch(transactionsRequest, [])) {
type: WithdrawalType.TalerBankIntegrationApi, return;
confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false, }
reservePub: wsr.reservePub, if (!pi.accepted) {
bankConfirmationUrl: wsr.bankInfo.confirmUrl, return;
}; }
} else { transactions.push({
const exchangeDetails = await getExchangeDetails( type: TransactionType.PeerPullDebit,
tx, amountEffective: Amounts.stringify(amount),
wsr.exchangeBaseUrl, amountRaw: Amounts.stringify(amount),
); exchangeBaseUrl: pi.exchangeBaseUrl,
if (!exchangeDetails) { frozen: false,
// FIXME: report somehow pending: false,
return; timestamp: pi.timestampCreated,
} transactionId: makeEventId(
withdrawalDetails = { TransactionType.PeerPullDebit,
type: WithdrawalType.ManualTransfer, pi.pursePub,
reservePub: wsr.reservePub, ),
exchangePaytoUris: });
exchangeDetails.wireInfo?.accounts.map((x) => `${x.payto_uri}?subject=${wsr.reservePub}`) ?? });
[],
};
}
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if (
shouldSkipCurrency(
transactionsRequest,
wsr.rawWithdrawalAmount.currency,
)
) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
let withdrawalDetails: WithdrawalDetails;
if (wsr.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
transactions.push({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.PeerPullCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish, pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart, timestamp: wsr.timestampStart,
transactionId: makeEventId( transactionId: makeEventId(
TransactionType.Withdrawal, TransactionType.PeerPullCredit,
wsr.withdrawalGroupId, wsr.withdrawalGroupId,
), ),
frozen: false, frozen: false,
...(wsr.lastError ? { error: wsr.lastError } : {}), ...(wsr.lastError ? { error: wsr.lastError } : {}),
}); });
}); return;
} else if (wsr.withdrawalType === WithdrawalRecordType.PeerPushCredit) {
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
transactions.push({ transactions.push({
type: TransactionType.Deposit, type: TransactionType.PeerPushCredit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountEffective: Amounts.stringify(dg.totalPayCost), amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
pending: !dg.timestampFinished, exchangeBaseUrl: wsr.exchangeBaseUrl,
frozen: false, pending: !wsr.timestampFinish,
timestamp: dg.timestampCreated, timestamp: wsr.timestampStart,
targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId( transactionId: makeEventId(
TransactionType.Deposit, TransactionType.PeerPushCredit,
dg.depositGroupId, wsr.withdrawalGroupId,
), ),
depositGroupId: dg.depositGroupId, frozen: false,
...(dg.lastError ? { error: dg.lastError } : {}), ...(wsr.lastError ? { error: wsr.lastError } : {}),
}); });
}); return;
} else if (wsr.bankInfo) {
tx.purchases.iter().forEachAsync(async (pr) => { withdrawalDetails = {
if ( type: WithdrawalType.TalerBankIntegrationApi,
shouldSkipCurrency( confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false,
transactionsRequest, reservePub: wsr.reservePub,
pr.download.contractData.amount.currency, bankConfirmationUrl: wsr.bankInfo.confirmUrl,
)
) {
return;
}
const contractData = pr.download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return;
}
const proposal = await tx.proposals.get(pr.proposalId);
if (!proposal) {
return;
}
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
}; };
if (contractData.fulfillmentUrl !== "") { } else {
info.fulfillmentUrl = contractData.fulfillmentUrl; const exchangeDetails = await getExchangeDetails(
} tx,
const paymentTransactionId = makeEventId( wsr.exchangeBaseUrl,
TransactionType.Payment,
pr.proposalId,
); );
const refundGroupKeys = new Set<string>(); if (!exchangeDetails) {
// FIXME: report somehow
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
reservePub: wsr.reservePub,
exchangePaytoUris:
exchangeDetails.wireInfo?.accounts.map(
(x) => `${x.payto_uri}?subject=${wsr.reservePub}`,
) ?? [],
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
frozen: false,
...(wsr.lastError ? { error: wsr.lastError } : {}),
});
});
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
transactions.push({
type: TransactionType.Deposit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
pending: !dg.timestampFinished,
frozen: false,
timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId(
TransactionType.Deposit,
dg.depositGroupId,
),
depositGroupId: dg.depositGroupId,
...(dg.lastError ? { error: dg.lastError } : {}),
});
});
tx.purchases.iter().forEachAsync(async (pr) => {
if (
shouldSkipCurrency(
transactionsRequest,
pr.download.contractData.amount.currency,
)
) {
return;
}
const contractData = pr.download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return;
}
const proposal = await tx.proposals.get(pr.proposalId);
if (!proposal) {
return;
}
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
};
if (contractData.fulfillmentUrl !== "") {
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
const paymentTransactionId = makeEventId(
TransactionType.Payment,
pr.proposalId,
);
const refundGroupKeys = new Set<string>();
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const groupKey = `${refund.executionTime.t_s}`;
refundGroupKeys.add(groupKey);
}
let totalRefundRaw = Amounts.getZero(contractData.amount.currency);
let totalRefundEffective = Amounts.getZero(
contractData.amount.currency,
);
const refunds: RefundInfoShort[] = [];
for (const groupKey of refundGroupKeys.values()) {
const refundTombstoneId = makeEventId(
TombstoneTag.DeleteRefund,
pr.proposalId,
groupKey,
);
const tombstone = await tx.tombstones.get(refundTombstoneId);
if (tombstone) {
continue;
}
const refundTransactionId = makeEventId(
TransactionType.Refund,
pr.proposalId,
groupKey,
);
let r0: WalletRefundItem | undefined;
let amountRaw = Amounts.getZero(contractData.amount.currency);
let amountEffective = Amounts.getZero(contractData.amount.currency);
for (const rk of Object.keys(pr.refunds)) { for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk]; const refund = pr.refunds[rk];
const groupKey = `${refund.executionTime.t_s}`; const myGroupKey = `${refund.executionTime.t_s}`;
refundGroupKeys.add(groupKey); if (myGroupKey !== groupKey) {
}
let totalRefundRaw = Amounts.getZero(contractData.amount.currency);
let totalRefundEffective = Amounts.getZero(
contractData.amount.currency,
);
const refunds: RefundInfoShort[] = [];
for (const groupKey of refundGroupKeys.values()) {
const refundTombstoneId = makeEventId(
TombstoneTag.DeleteRefund,
pr.proposalId,
groupKey,
);
const tombstone = await tx.tombstones.get(refundTombstoneId);
if (tombstone) {
continue; continue;
} }
const refundTransactionId = makeEventId(
TransactionType.Refund,
pr.proposalId,
groupKey,
);
let r0: WalletRefundItem | undefined;
let amountRaw = Amounts.getZero(contractData.amount.currency);
let amountEffective = Amounts.getZero(contractData.amount.currency);
for (const rk of Object.keys(pr.refunds)) {
const refund = pr.refunds[rk];
const myGroupKey = `${refund.executionTime.t_s}`;
if (myGroupKey !== groupKey) {
continue;
}
if (!r0) {
r0 = refund;
}
if (refund.type === RefundState.Applied) {
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
amountEffective = Amounts.add(
amountEffective,
Amounts.sub(
refund.refundAmount,
refund.refundFee,
refund.totalRefreshCostBound,
).amount,
).amount;
refunds.push({
transactionId: refundTransactionId,
timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective),
amountRaw: Amounts.stringify(amountRaw),
});
}
}
if (!r0) { if (!r0) {
throw Error("invariant violated"); r0 = refund;
} }
totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; if (refund.type === RefundState.Applied) {
totalRefundEffective = Amounts.add( amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
totalRefundEffective, amountEffective = Amounts.add(
amountEffective, amountEffective,
).amount; Amounts.sub(
transactions.push({ refund.refundAmount,
type: TransactionType.Refund, refund.refundFee,
info, refund.totalRefreshCostBound,
refundedTransactionId: paymentTransactionId, ).amount,
transactionId: refundTransactionId, ).amount;
timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective), refunds.push({
amountRaw: Amounts.stringify(amountRaw), transactionId: refundTransactionId,
refundPending: timestamp: r0.obtainedTime,
pr.refundAwaiting === undefined amountEffective: Amounts.stringify(amountEffective),
? undefined amountRaw: Amounts.stringify(amountRaw),
: Amounts.stringify(pr.refundAwaiting), });
pending: false, }
frozen: false, }
}); if (!r0) {
throw Error("invariant violated");
} }
const err = pr.lastPayError ?? pr.lastRefundStatusError; totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount;
totalRefundEffective = Amounts.add(
totalRefundEffective,
amountEffective,
).amount;
transactions.push({ transactions.push({
type: TransactionType.Payment, type: TransactionType.Refund,
amountRaw: Amounts.stringify(contractData.amount), info,
amountEffective: Amounts.stringify(pr.totalPayCost), refundedTransactionId: paymentTransactionId,
totalRefundRaw: Amounts.stringify(totalRefundRaw), transactionId: refundTransactionId,
totalRefundEffective: Amounts.stringify(totalRefundEffective), timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective),
amountRaw: Amounts.stringify(amountRaw),
refundPending: refundPending:
pr.refundAwaiting === undefined pr.refundAwaiting === undefined
? undefined ? undefined
: Amounts.stringify(pr.refundAwaiting), : Amounts.stringify(pr.refundAwaiting),
status: pr.timestampFirstSuccessfulPay pending: false,
? PaymentStatus.Paid
: PaymentStatus.Accepted,
pending:
!pr.timestampFirstSuccessfulPay &&
pr.abortStatus === AbortStatus.None,
refunds,
timestamp: pr.timestampAccept,
transactionId: paymentTransactionId,
proposalId: pr.proposalId,
info,
frozen: pr.payFrozen ?? false,
...(err ? { error: err } : {}),
});
});
tx.tips.iter().forEachAsync(async (tipRecord) => {
if (
shouldSkipCurrency(
transactionsRequest,
tipRecord.tipAmountRaw.currency,
)
) {
return;
}
if (!tipRecord.acceptedTimestamp) {
return;
}
transactions.push({
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp,
frozen: false, frozen: false,
timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId(
TransactionType.Tip,
tipRecord.walletTipId,
),
merchantBaseUrl: tipRecord.merchantBaseUrl,
// merchant: {
// name: tipRecord.merchantBaseUrl,
// },
error: tipRecord.lastError,
}); });
}
const err = pr.lastPayError ?? pr.lastRefundStatusError;
transactions.push({
type: TransactionType.Payment,
amountRaw: Amounts.stringify(contractData.amount),
amountEffective: Amounts.stringify(pr.totalPayCost),
totalRefundRaw: Amounts.stringify(totalRefundRaw),
totalRefundEffective: Amounts.stringify(totalRefundEffective),
refundPending:
pr.refundAwaiting === undefined
? undefined
: Amounts.stringify(pr.refundAwaiting),
status: pr.timestampFirstSuccessfulPay
? PaymentStatus.Paid
: PaymentStatus.Accepted,
pending:
!pr.timestampFirstSuccessfulPay &&
pr.abortStatus === AbortStatus.None,
refunds,
timestamp: pr.timestampAccept,
transactionId: paymentTransactionId,
proposalId: pr.proposalId,
info,
frozen: pr.payFrozen ?? false,
...(err ? { error: err } : {}),
}); });
}, });
);
tx.tips.iter().forEachAsync(async (tipRecord) => {
if (
shouldSkipCurrency(
transactionsRequest,
tipRecord.tipAmountRaw.currency,
)
) {
return;
}
if (!tipRecord.acceptedTimestamp) {
return;
}
transactions.push({
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp,
frozen: false,
timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId(
TransactionType.Tip,
tipRecord.walletTipId,
),
merchantBaseUrl: tipRecord.merchantBaseUrl,
// merchant: {
// name: tipRecord.merchantBaseUrl,
// },
error: tipRecord.lastError,
});
});
});
const txPending = transactions.filter((x) => x.pending); const txPending = transactions.filter((x) => x.pending);
const txNotPending = transactions.filter((x) => !x.pending); const txNotPending = transactions.filter((x) => !x.pending);

View File

@ -74,6 +74,7 @@ import {
ReserveRecordStatus, ReserveRecordStatus,
WalletStoresV1, WalletStoresV1,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WithdrawalRecordType,
} from "../db.js"; } from "../db.js";
import { import {
getErrorDetailFromException, getErrorDetailFromException,
@ -1700,6 +1701,7 @@ export async function internalCreateWithdrawalGroup(
forcedDenomSel?: ForcedDenomSel; forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair; reserveKeyPair?: EddsaKeypair;
restrictAge?: number; restrictAge?: number;
withdrawalType: WithdrawalRecordType;
}, },
): Promise<WithdrawalGroupRecord> { ): Promise<WithdrawalGroupRecord> {
const reserveKeyPair = const reserveKeyPair =
@ -1745,6 +1747,7 @@ export async function internalCreateWithdrawalGroup(
restrictAge: args.restrictAge, restrictAge: args.restrictAge,
senderWire: undefined, senderWire: undefined,
timestampFinish: undefined, timestampFinish: undefined,
withdrawalType: args.withdrawalType,
}; };
const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange); const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
@ -1819,6 +1822,7 @@ export async function acceptWithdrawalFromUri(
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, { const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
amount: withdrawInfo.amount, amount: withdrawInfo.amount,
exchangeBaseUrl: req.selectedExchange, exchangeBaseUrl: req.selectedExchange,
withdrawalType: WithdrawalRecordType.BankIntegrated,
forcedDenomSel: req.forcedDenomSel, forcedDenomSel: req.forcedDenomSel,
reserveStatus: ReserveRecordStatus.RegisteringBank, reserveStatus: ReserveRecordStatus.RegisteringBank,
bankInfo: { bankInfo: {
@ -1877,6 +1881,7 @@ export async function createManualWithdrawal(
): Promise<AcceptManualWithdrawalResult> { ): Promise<AcceptManualWithdrawalResult> {
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, { const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
amount: Amounts.jsonifyAmount(req.amount), amount: Amounts.jsonifyAmount(req.amount),
withdrawalType: WithdrawalRecordType.BankManual,
exchangeBaseUrl: req.exchangeBaseUrl, exchangeBaseUrl: req.exchangeBaseUrl,
bankInfo: undefined, bankInfo: undefined,
forcedDenomSel: req.forcedDenomSel, forcedDenomSel: req.forcedDenomSel,