diff options
Diffstat (limited to 'packages')
7 files changed, 524 insertions, 273 deletions
| diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index 645f0120b..c4bd3d464 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -102,7 +102,11 @@ export type Transaction =    | TransactionRefund    | TransactionTip    | TransactionRefresh -  | TransactionDeposit; +  | TransactionDeposit +  | TransactionPeerPullCredit +  | TransactionPeerPullDebit +  | TransactionPeerPushCredit +  | TransactionPeerPushDebit;  export enum TransactionType {    Withdrawal = "withdrawal", @@ -111,6 +115,10 @@ export enum TransactionType {    Refresh = "refresh",    Tip = "tip",    Deposit = "deposit", +  PeerPushDebit = "peer-push-debit", +  PeerPushCredit = "peer-push-credit", +  PeerPullDebit = "peer-pull-debit", +  PeerPullCredit = "peer-pull-credit",  }  export enum WithdrawalType { @@ -179,6 +187,76 @@ export interface TransactionWithdrawal extends TransactionCommon {    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 {    /**     * Explicitly aborted after timeout / failure @@ -311,10 +389,10 @@ export interface OrderShortInfo {  }  export interface RefundInfoShort { -  transactionId: string, -  timestamp: TalerProtocolTimestamp, -  amountEffective: AmountString, -  amountRaw: AmountString, +  transactionId: string; +  timestamp: TalerProtocolTimestamp; +  amountEffective: AmountString; +  amountRaw: AmountString;  }  export interface TransactionRefund extends TransactionCommon { diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts index 1be1563ce..0c149d63a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts @@ -19,7 +19,7 @@   */  import { j2s } from "@gnu-taler/taler-util";  import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; +import { GlobalTestState, WalletCli } from "../harness/harness.js";  import {    createSimpleTestkudosEnvironment,    withdrawViaBank, @@ -31,16 +31,23 @@ import {  export async function runPeerToPeerPullTest(t: GlobalTestState) {    // Set up test environment -  const { wallet, bank, exchange, merchant } = -    await createSimpleTestkudosEnvironment(t); +  const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment( +    t, +  );    // Withdraw digital cash into the wallet. - -  await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - -  await wallet.runUntilDone(); - -  const resp = await wallet.client.call( +  const wallet1 = new WalletCli(t, "w1"); +  const wallet2 = new WalletCli(t, "w2"); +  await withdrawViaBank(t, { +    wallet: wallet2, +    bank, +    exchange, +    amount: "TESTKUDOS:20", +  }); + +  await wallet1.runUntilDone(); + +  const resp = await wallet1.client.call(      WalletApiOperation.InitiatePeerPullPayment,      {        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,      {        talerUri: resp.talerUri, @@ -60,18 +67,27 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {    console.log(`checkResp: ${j2s(checkResp)}`); -  const acceptResp = await wallet.client.call( +  const acceptResp = await wallet2.client.call(      WalletApiOperation.AcceptPeerPullPayment,      {        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"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts index bf65731d2..ebbe87ae8 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts @@ -17,6 +17,7 @@  /**   * Imports.   */ +import { j2s } from "@gnu-taler/taler-util";  import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";  import { GlobalTestState, WalletCli } from "../harness/harness.js";  import { @@ -78,6 +79,18 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {    await wallet1.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"]; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 3d59ce0a7..e6b4854db 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -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.   * (Either for a normal withdrawal or from a tip.) @@ -1232,6 +1239,8 @@ export interface WithdrawalGroupRecord {     */    withdrawalGroupId: string; +  withdrawalType: WithdrawalRecordType; +    /**     * Secret seed used to derive planchets.     * Stored since planchets are created lazily. @@ -1607,8 +1616,6 @@ export interface PeerPushPaymentInitiationRecord {    contractPriv: string; -  contractPub: string; -    purseExpiration: TalerProtocolTimestamp;    /** @@ -1681,7 +1688,11 @@ export interface PeerPullPaymentIncomingRecord {    contractTerms: PeerContractTerms; -  timestamp: TalerProtocolTimestamp; +  timestampCreated: TalerProtocolTimestamp; + +  paid: boolean; + +  accepted: boolean;    contractPriv: string;  } @@ -1878,9 +1889,18 @@ export const WalletStoresV1 = {        ]),      },    ), -  peerPullPaymentInitiation: describeStore( +  peerPullPaymentInitiations: describeStore(      describeContents<PeerPullPaymentInitiationRecord>( -      "peerPushPaymentInitiation", +      "peerPullPaymentInitiations", +      { +        keyPath: "pursePub", +      }, +    ), +    {}, +  ), +  peerPushPaymentInitiations: describeStore( +    describeContents<PeerPushPaymentInitiationRecord>( +      "peerPushPaymentInitiations",        {          keyPath: "pursePub",        }, diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index ddfaa0827..d6d71720c 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -65,6 +65,7 @@ import {    MergeReserveInfo,    ReserveRecordStatus,    WalletStoresV1, +  WithdrawalRecordType,  } from "../db.js";  import { readSuccessResponseJsonOrThrow } from "../util/http.js";  import { InternalWalletState } from "../internal-wallet-state.js"; @@ -208,12 +209,39 @@ export async function initiatePeerToPeerPush(  ): Promise<InitiatePeerPushPaymentResponse> {    // FIXME: actually create a record for retries here!    const instructedAmount = Amounts.parseOrThrow(req.amount); + +  const pursePair = await ws.cryptoApi.createEddsaKeypair({}); +  const mergePair = await ws.cryptoApi.createEddsaKeypair({}); + +  const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( +    AbsoluteTime.addDuration( +      AbsoluteTime.now(), +      Duration.fromSpec({ days: 2 }), +    ), +  ); + +  const contractTerms = { +    ...req.partialContractTerms, +    purse_expiration: purseExpiration, +    amount: req.amount, +  }; + +  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); @@ -232,6 +260,20 @@ export async function initiatePeerToPeerPush(          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; @@ -242,24 +284,6 @@ export async function initiatePeerToPeerPush(      throw Error("insufficient balance");    } -  const pursePair = await ws.cryptoApi.createEddsaKeypair({}); -  const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - -  const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( -    AbsoluteTime.addDuration( -      AbsoluteTime.now(), -      Duration.fromSpec({ days: 2 }), -    ), -  ); - -  const contractTerms = { -    ...req.partialContractTerms, -    purse_expiration: purseExpiration, -    amount: req.amount, -  }; - -  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); -    const purseSigResp = await ws.cryptoApi.signPurseCreation({      hContractTerms,      mergePub: mergePair.pub, @@ -280,13 +304,6 @@ export async function initiatePeerToPeerPush(      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, {      amount: Amounts.stringify(instructedAmount),      merge_pub: mergePair.pub, @@ -517,6 +534,7 @@ export async function acceptPeerPushPayment(    await internalCreateWithdrawalGroup(ws, {      amount, +    withdrawalType: WithdrawalRecordType.PeerPushCredit,      exchangeBaseUrl: peerInc.exchangeBaseUrl,      reserveStatus: ReserveRecordStatus.QueryingStatus,      reserveKeyPair: { @@ -554,6 +572,7 @@ export async function acceptPeerPullPayment(        coins: x.coins,        denominations: x.denominations,        refreshGroups: x.refreshGroups, +      peerPullPaymentIncoming: x.peerPullPaymentIncoming,      }))      .runReadWrite(async (tx) => {        const sel = await selectPeerCoins(ws, tx, instructedAmount); @@ -574,6 +593,15 @@ export async function acceptPeerPullPayment(        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;      });    logger.info(`selected p2p coins: ${j2s(coinSelRes)}`); @@ -656,8 +684,10 @@ export async function checkPeerPullPayment(          contractPriv: contractPriv,          exchangeBaseUrl: exchangeBaseUrl,          pursePub: pursePub, -        timestamp: TalerProtocolTimestamp.now(), +        timestampCreated: TalerProtocolTimestamp.now(),          contractTerms: dec.contractTerms, +        paid: false, +        accepted: false,        });      }); @@ -672,6 +702,8 @@ export async function initiatePeerRequestForPay(    ws: InternalWalletState,    req: InitiatePeerPullPaymentRequest,  ): Promise<InitiatePeerPullPaymentResponse> { +  await updateExchangeFromUrl(ws, req.exchangeBaseUrl); +    const mergeReserveInfo = await getMergeReserveInfo(ws, {      exchangeBaseUrl: req.exchangeBaseUrl,    }); @@ -727,7 +759,7 @@ export async function initiatePeerRequestForPay(    await ws.db      .mktx((x) => ({ -      peerPullPaymentInitiation: x.peerPullPaymentInitiation, +      peerPullPaymentInitiation: x.peerPullPaymentInitiations,      }))      .runReadWrite(async (tx) => {        await tx.peerPullPaymentInitiation.put({ @@ -772,6 +804,7 @@ export async function initiatePeerRequestForPay(    await internalCreateWithdrawalGroup(ws, {      amount: Amounts.parseOrThrow(req.amount), +    withdrawalType: WithdrawalRecordType.PeerPullCredit,      exchangeBaseUrl: req.exchangeBaseUrl,      reserveStatus: ReserveRecordStatus.QueryingStatus,      reserveKeyPair: { diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index ec499420f..62df996c3 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -38,6 +38,7 @@ import {    RefundState,    ReserveRecordStatus,    WalletRefundItem, +  WithdrawalRecordType,  } from "../db.js";  import { processDepositGroup } from "./deposits.js";  import { getExchangeDetails } from "./exchanges.js"; @@ -101,10 +102,14 @@ const txOrder: { [t in TransactionType]: number } = {    [TransactionType.Withdrawal]: 1,    [TransactionType.Tip]: 2,    [TransactionType.Payment]: 3, -  [TransactionType.Refund]: 4, -  [TransactionType.Deposit]: 5, -  [TransactionType.Refresh]: 6, -  [TransactionType.Tip]: 7, +  [TransactionType.PeerPullCredit]: 4, +  [TransactionType.PeerPullDebit]: 5, +  [TransactionType.PeerPushCredit]: 6, +  [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,        depositGroups: x.depositGroups,        tombstones: x.tombstones, +      peerPushPaymentInitiations: x.peerPushPaymentInitiations, +      peerPullPaymentIncoming: x.peerPullPaymentIncoming,      })) -    .runReadOnly( -      // Report withdrawals that are currently in progress. -      async (tx) => { -        tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { -          if ( -            shouldSkipCurrency( -              transactionsRequest, -              wsr.rawWithdrawalAmount.currency, -            ) -          ) { -            return; -          } +    .runReadOnly(async (tx) => { +      tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => { +        const amount = Amounts.parseOrThrow(pi.amount); +        if (shouldSkipCurrency(transactionsRequest, amount.currency)) { +          return; +        } +        if (shouldSkipSearch(transactionsRequest, [])) { +          return; +        } +        transactions.push({ +          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, [])) { -            return; -          } -          let withdrawalDetails: WithdrawalDetails; -          if (wsr.bankInfo) { -            withdrawalDetails = { -              type: WithdrawalType.TalerBankIntegrationApi, -              confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false, -              reservePub: wsr.reservePub, -              bankConfirmationUrl: wsr.bankInfo.confirmUrl, -            }; -          } else { -            const exchangeDetails = await getExchangeDetails( -              tx, -              wsr.exchangeBaseUrl, -            ); -            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}`) ?? -                [], -            }; -          } +      tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => { +        const amount = Amounts.parseOrThrow(pi.contractTerms.amount); +        if (shouldSkipCurrency(transactionsRequest, amount.currency)) { +          return; +        } +        if (shouldSkipSearch(transactionsRequest, [])) { +          return; +        } +        if (!pi.accepted) { +          return; +        } +        transactions.push({ +          type: TransactionType.PeerPullDebit, +          amountEffective: Amounts.stringify(amount), +          amountRaw: Amounts.stringify(amount), +          exchangeBaseUrl: pi.exchangeBaseUrl, +          frozen: false, +          pending: false, +          timestamp: pi.timestampCreated, +          transactionId: makeEventId( +            TransactionType.PeerPullDebit, +            pi.pursePub, +          ), +        }); +      }); +      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({ -            type: TransactionType.Withdrawal, +            type: TransactionType.PeerPullCredit,              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, +              TransactionType.PeerPullCredit,                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; -          } - +          return; +        } else if (wsr.withdrawalType === WithdrawalRecordType.PeerPushCredit) {            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, +            type: TransactionType.PeerPushCredit, +            amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), +            amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount), +            exchangeBaseUrl: wsr.exchangeBaseUrl, +            pending: !wsr.timestampFinish, +            timestamp: wsr.timestampStart,              transactionId: makeEventId( -              TransactionType.Deposit, -              dg.depositGroupId, +              TransactionType.PeerPushCredit, +              wsr.withdrawalGroupId,              ), -            depositGroupId: dg.depositGroupId, -            ...(dg.lastError ? { error: dg.lastError } : {}), +            frozen: false, +            ...(wsr.lastError ? { error: wsr.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; +        } else if (wsr.bankInfo) { +          withdrawalDetails = { +            type: WithdrawalType.TalerBankIntegrationApi, +            confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false, +            reservePub: wsr.reservePub, +            bankConfirmationUrl: wsr.bankInfo.confirmUrl, +          }; +        } else { +          const exchangeDetails = await getExchangeDetails( +            tx, +            wsr.exchangeBaseUrl, +          ); +          if (!exchangeDetails) { +            // FIXME: report somehow              return;            } -          const info: OrderShortInfo = { -            merchant: contractData.merchant, -            orderId: contractData.orderId, -            products: contractData.products, -            summary: contractData.summary, -            summary_i18n: contractData.summaryI18n, -            contractTermsHash: contractData.contractTermsHash, +          withdrawalDetails = { +            type: WithdrawalType.ManualTransfer, +            reservePub: wsr.reservePub, +            exchangePaytoUris: +              exchangeDetails.wireInfo?.accounts.map( +                (x) => `${x.payto_uri}?subject=${wsr.reservePub}`, +              ) ?? [],            }; -          if (contractData.fulfillmentUrl !== "") { -            info.fulfillmentUrl = contractData.fulfillmentUrl; +        } + +        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 paymentTransactionId = makeEventId( -            TransactionType.Payment, +          const refundTransactionId = makeEventId( +            TransactionType.Refund,              pr.proposalId, +            groupKey,            ); -          const refundGroupKeys = new Set<string>(); - +          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 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) { +            const myGroupKey = `${refund.executionTime.t_s}`; +            if (myGroupKey !== groupKey) {                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) { -              throw Error("invariant violated"); +              r0 = refund;              } -            totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; -            totalRefundEffective = Amounts.add( -              totalRefundEffective, -              amountEffective, -            ).amount; -            transactions.push({ -              type: TransactionType.Refund, -              info, -              refundedTransactionId: paymentTransactionId, -              transactionId: refundTransactionId, -              timestamp: r0.obtainedTime, -              amountEffective: Amounts.stringify(amountEffective), -              amountRaw: Amounts.stringify(amountRaw), -              refundPending: -                pr.refundAwaiting === undefined -                  ? undefined -                  : Amounts.stringify(pr.refundAwaiting), -              pending: false, -              frozen: false, -            }); +            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) { +            throw Error("invariant violated");            } -          const err = pr.lastPayError ?? pr.lastRefundStatusError; +          totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; +          totalRefundEffective = Amounts.add( +            totalRefundEffective, +            amountEffective, +          ).amount;            transactions.push({ -            type: TransactionType.Payment, -            amountRaw: Amounts.stringify(contractData.amount), -            amountEffective: Amounts.stringify(pr.totalPayCost), -            totalRefundRaw: Amounts.stringify(totalRefundRaw), -            totalRefundEffective: Amounts.stringify(totalRefundEffective), +            type: TransactionType.Refund, +            info, +            refundedTransactionId: paymentTransactionId, +            transactionId: refundTransactionId, +            timestamp: r0.obtainedTime, +            amountEffective: Amounts.stringify(amountEffective), +            amountRaw: Amounts.stringify(amountRaw),              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 } : {}), +            pending: false, +            frozen: false,            }); +        } + +        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, -          }); +      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 txNotPending = transactions.filter((x) => !x.pending); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 3c4e2d98c..4e350670d 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -74,6 +74,7 @@ import {    ReserveRecordStatus,    WalletStoresV1,    WithdrawalGroupRecord, +  WithdrawalRecordType,  } from "../db.js";  import {    getErrorDetailFromException, @@ -1700,6 +1701,7 @@ export async function internalCreateWithdrawalGroup(      forcedDenomSel?: ForcedDenomSel;      reserveKeyPair?: EddsaKeypair;      restrictAge?: number; +    withdrawalType: WithdrawalRecordType;    },  ): Promise<WithdrawalGroupRecord> {    const reserveKeyPair = @@ -1745,6 +1747,7 @@ export async function internalCreateWithdrawalGroup(      restrictAge: args.restrictAge,      senderWire: undefined,      timestampFinish: undefined, +    withdrawalType: args.withdrawalType,    };    const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange); @@ -1819,6 +1822,7 @@ export async function acceptWithdrawalFromUri(    const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {      amount: withdrawInfo.amount,      exchangeBaseUrl: req.selectedExchange, +    withdrawalType: WithdrawalRecordType.BankIntegrated,      forcedDenomSel: req.forcedDenomSel,      reserveStatus: ReserveRecordStatus.RegisteringBank,      bankInfo: { @@ -1877,6 +1881,7 @@ export async function createManualWithdrawal(  ): Promise<AcceptManualWithdrawalResult> {    const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {      amount: Amounts.jsonifyAmount(req.amount), +    withdrawalType: WithdrawalRecordType.BankManual,      exchangeBaseUrl: req.exchangeBaseUrl,      bankInfo: undefined,      forcedDenomSel: req.forcedDenomSel, | 
