wallet-core: towards DD37 for deposits

This commit is contained in:
Florian Dold 2023-04-22 14:17:49 +02:00
parent e331012c9f
commit 15feebecfe
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 314 additions and 111 deletions

View File

@ -970,6 +970,19 @@ export class ExchangeService implements ExchangeServiceInterface {
); );
} }
async runAggregatorOnceWithTimetravel(opts: {
timetravelMicroseconds: number;
}) {
let timetravelArgArr = [];
timetravelArgArr.push(`--timetravel=${opts.timetravelMicroseconds}`);
await runCommand(
this.globalState,
`exchange-${this.name}-aggregator-once`,
"taler-exchange-aggregator",
[...timetravelArgArr, "-c", this.configFilename, "-t"],
);
}
async runAggregatorOnce() { async runAggregatorOnce() {
try { try {
await runCommand( await runCommand(
@ -1147,6 +1160,9 @@ export class ExchangeService implements ExchangeServiceInterface {
exchangeHttpProc: ProcessWrapper | undefined; exchangeHttpProc: ProcessWrapper | undefined;
exchangeWirewatchProc: ProcessWrapper | undefined; exchangeWirewatchProc: ProcessWrapper | undefined;
exchangeTransferProc: ProcessWrapper | undefined;
exchangeAggregatorProc: ProcessWrapper | undefined;
helperCryptoRsaProc: ProcessWrapper | undefined; helperCryptoRsaProc: ProcessWrapper | undefined;
helperCryptoEddsaProc: ProcessWrapper | undefined; helperCryptoEddsaProc: ProcessWrapper | undefined;
helperCryptoCsProc: ProcessWrapper | undefined; helperCryptoCsProc: ProcessWrapper | undefined;
@ -1200,6 +1216,18 @@ export class ExchangeService implements ExchangeServiceInterface {
await wirewatch.wait(); await wirewatch.wait();
this.exchangeWirewatchProc = undefined; this.exchangeWirewatchProc = undefined;
} }
const aggregatorProc = this.exchangeAggregatorProc;
if (aggregatorProc) {
aggregatorProc.proc.kill("SIGTERM");
await aggregatorProc.wait();
this.exchangeAggregatorProc = undefined;
}
const transferProc = this.exchangeTransferProc;
if (transferProc) {
transferProc.proc.kill("SIGTERM");
await transferProc.wait();
this.exchangeTransferProc = undefined;
}
const httpd = this.exchangeHttpProc; const httpd = this.exchangeHttpProc;
if (httpd) { if (httpd) {
httpd.proc.kill("SIGTERM"); httpd.proc.kill("SIGTERM");
@ -1369,6 +1397,22 @@ export class ExchangeService implements ExchangeServiceInterface {
); );
} }
private internalCreateAggregatorProc() {
this.exchangeAggregatorProc = this.globalState.spawnService(
"taler-exchange-aggregator",
["-c", this.configFilename, ...this.timetravelArgArr],
`exchange-aggregator-${this.name}`,
);
}
private internalCreateTransferProc() {
this.exchangeTransferProc = this.globalState.spawnService(
"taler-exchange-transfer",
["-c", this.configFilename, ...this.timetravelArgArr],
`exchange-transfer-${this.name}`,
);
}
async start(): Promise<void> { async start(): Promise<void> {
if (this.isRunning()) { if (this.isRunning()) {
throw Error("exchange is already running"); throw Error("exchange is already running");
@ -1398,6 +1442,8 @@ export class ExchangeService implements ExchangeServiceInterface {
); );
this.internalCreateWirewatchProc(); this.internalCreateWirewatchProc();
this.internalCreateTransferProc();
this.internalCreateAggregatorProc();
this.exchangeHttpProc = this.globalState.spawnService( this.exchangeHttpProc = this.globalState.spawnService(
"taler-exchange-httpd", "taler-exchange-httpd",
@ -2062,7 +2108,7 @@ export class WalletService {
[ [
"--wallet-db", "--wallet-db",
dbPath, dbPath,
"-LDEBUG", // FIXME: Make this configurable? "-LTRACE", // FIXME: Make this configurable?
"--no-throttle", // FIXME: Optionally do throttling for some tests? "--no-throttle", // FIXME: Optionally do throttling for some tests?
"advanced", "advanced",
"serve", "serve",

View File

@ -47,7 +47,14 @@ import { lintExchangeDeployment } from "./lint.js";
import { runEnvFull } from "./env-full.js"; import { runEnvFull } from "./env-full.js";
import { clk } from "@gnu-taler/taler-util/clk"; import { clk } from "@gnu-taler/taler-util/clk";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { BankAccessApiClient } from "@gnu-taler/taler-wallet-core"; import {
BankAccessApiClient,
checkReserve,
CryptoDispatcher,
downloadExchangeInfo,
SynchronousCryptoWorkerFactoryPlain,
topupReserveWithDemobank,
} from "@gnu-taler/taler-wallet-core";
const logger = new Logger("taler-harness:index.ts"); const logger = new Logger("taler-harness:index.ts");
@ -162,7 +169,6 @@ advancedCli
await runTestWithState(testState, runEnv1, "env1", true); await runTestWithState(testState, runEnv1, "env1", true);
}); });
const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", { const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", {
help: "Subcommands for handling GNU Taler sandcastle deployments.", help: "Subcommands for handling GNU Taler sandcastle deployments.",
}); });
@ -260,6 +266,66 @@ deploymentCli
// FIXME: Now delete reserves that are not filled yet // FIXME: Now delete reserves that are not filled yet
}); });
deploymentCli
.subcommand("testTalerdotnetDemo", "test-demo-talerdotnet")
.action(async (args) => {
const http = createPlatformHttpLib();
const cryptiDisp = new CryptoDispatcher(
new SynchronousCryptoWorkerFactoryPlain(),
);
const cryptoApi = cryptiDisp.cryptoApi;
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "https://exchange.demo.taler.net/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "KUDOS:10",
bankAccessApiBaseUrl:
"https://bank.demo.taler.net/demobanks/default/access-api/",
bankBaseUrl: "",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
});
let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "30000");
console.log("requesting", reserveUrl.href);
const longpollReq = http.fetch(reserveUrl.href, {
method: "GET",
});
const reserveStatusResp = await longpollReq;
console.log("reserve status", reserveStatusResp.status);
});
deploymentCli
.subcommand("testLocalhostDemo", "test-demo-localhost")
.action(async (args) => {
// Run checks against the "env-full" demo deployment on localhost
const http = createPlatformHttpLib();
const cryptiDisp = new CryptoDispatcher(
new SynchronousCryptoWorkerFactoryPlain(),
);
const cryptoApi = cryptiDisp.cryptoApi;
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "http://localhost:8081/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "TESTKUDOS:10",
bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
bankBaseUrl: "",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
});
let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "30000");
console.log("requesting", reserveUrl.href);
const longpollReq = http.fetch(reserveUrl.href, {
method: "GET",
});
const reserveStatusResp = await longpollReq;
console.log("reserve status", reserveStatusResp.status);
});
deploymentCli deploymentCli
.subcommand("tipStatus", "tip-status") .subcommand("tipStatus", "tip-status")
.requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING) .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)

View File

@ -17,7 +17,11 @@
/** /**
* Imports. * Imports.
*/ */
import { NotificationType, TransactionState } from "@gnu-taler/taler-util"; import {
NotificationType,
TransactionMajorState,
TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState, getPayto } from "../harness/harness.js"; import { GlobalTestState, getPayto } from "../harness/harness.js";
import { import {
@ -52,11 +56,19 @@ export async function runDepositTest(t: GlobalTestState) {
const depositTxId = dgIdResp.transactionId; const depositTxId = dgIdResp.transactionId;
const depositTrack = walletClient.waitForNotificationCond(
(n) =>
n.type == NotificationType.TransactionStateTransition &&
n.transactionId == depositTxId &&
n.newTxState.major == TransactionMajorState.Pending &&
n.newTxState.minor == TransactionMinorState.Track,
);
const depositDone = walletClient.waitForNotificationCond( const depositDone = walletClient.waitForNotificationCond(
(n) => (n) =>
n.type == NotificationType.TransactionStateTransition && n.type == NotificationType.TransactionStateTransition &&
n.transactionId == depositTxId && n.transactionId == depositTxId &&
n.newTxState == TransactionState.Done, n.newTxState.major == TransactionMajorState.Done,
); );
const depositGroupResult = await walletClient.client.call( const depositGroupResult = await walletClient.client.call(
@ -70,6 +82,12 @@ export async function runDepositTest(t: GlobalTestState) {
t.assertDeepEqual(depositGroupResult.transactionId, depositTxId); t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
await depositTrack;
await exchange.runAggregatorOnceWithTimetravel({
timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
});
await depositDone; await depositDone;
const transactions = await walletClient.client.call( const transactions = await walletClient.client.call(

View File

@ -59,16 +59,29 @@ export async function runWalletDblessTest(t: GlobalTestState) {
const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
await topupReserveWithDemobank( let reserveUrl = new URL(
http, `reserves/${reserveKeyPair.pub}`,
reserveKeyPair.pub, exchange.baseUrl,
bank.baseUrl,
bank.bankAccessApiBaseUrl,
exchangeInfo,
"TESTKUDOS:10",
); );
reserveUrl.searchParams.set("timeout_ms", "30000");
const longpollReq = http.fetch(reserveUrl.href, {
method: "GET",
});
await exchange.runWirewatchOnce(); await topupReserveWithDemobank({
amount: "TESTKUDOS:10",
http,
reservePub: reserveKeyPair.pub,
bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
bankBaseUrl: bank.baseUrl,
exchangeInfo,
});
console.log("waiting for longpoll request");
const resp = await longpollReq;
console.log(`got response, status ${resp.status}`);
console.log(exchangeInfo);
await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);

View File

@ -22,7 +22,7 @@
/** /**
* Imports. * Imports.
*/ */
import { TransactionState, TransactionSubstate } from "./transactions-types.js"; import { TransactionState } from "./transactions-types.js";
import { TalerErrorDetail } from "./wallet-types.js"; import { TalerErrorDetail } from "./wallet-types.js";
export enum NotificationType { export enum NotificationType {
@ -75,9 +75,7 @@ export interface TransactionStateTransitionNotification {
type: NotificationType.TransactionStateTransition; type: NotificationType.TransactionStateTransition;
transactionId: string; transactionId: string;
oldTxState: TransactionState; oldTxState: TransactionState;
oldTxSubstate: TransactionSubstate;
newTxState: TransactionState; newTxState: TransactionState;
newTxSubstate: TransactionSubstate;
} }
export interface ProposalAcceptedNotification { export interface ProposalAcceptedNotification {

View File

@ -16,10 +16,9 @@
import { import {
TransactionType, TransactionType,
TransactionState,
TransactionSubstate,
PaymentStatus, PaymentStatus,
ExtendedStatus, ExtendedStatus,
TransactionMajorState,
} from "./transactions-types.js"; } from "./transactions-types.js";
import { RefreshReason } from "./wallet-types.js"; import { RefreshReason } from "./wallet-types.js";
@ -29,8 +28,9 @@ import { RefreshReason } from "./wallet-types.js";
export const sampleWalletCoreTransactions = [ export const sampleWalletCoreTransactions = [
{ {
type: TransactionType.Payment, type: TransactionType.Payment,
txState: TransactionState.Done, txState: {
txSubstate: TransactionSubstate.None, major: TransactionMajorState.Done,
},
amountRaw: "KUDOS:10", amountRaw: "KUDOS:10",
amountEffective: "KUDOS:10", amountEffective: "KUDOS:10",
totalRefundRaw: "KUDOS:0", totalRefundRaw: "KUDOS:0",
@ -75,8 +75,9 @@ export const sampleWalletCoreTransactions = [
}, },
{ {
type: TransactionType.Refresh, type: TransactionType.Refresh,
txState: TransactionState.Pending, txState: {
txSubstate: TransactionSubstate.None, major: TransactionMajorState.Pending,
},
refreshReason: RefreshReason.PayMerchant, refreshReason: RefreshReason.PayMerchant,
amountEffective: "KUDOS:0", amountEffective: "KUDOS:0",
amountRaw: "KUDOS:0", amountRaw: "KUDOS:0",

View File

@ -59,11 +59,6 @@ export enum ExtendedStatus {
KycRequired = "kyc-required", KycRequired = "kyc-required",
} }
export interface TransactionStateInfo {
txState: TransactionState;
txSubstate: TransactionSubstate;
}
export interface TransactionsRequest { export interface TransactionsRequest {
/** /**
* return only transactions in the given currency * return only transactions in the given currency
@ -81,7 +76,12 @@ export interface TransactionsRequest {
includeRefreshes?: boolean; includeRefreshes?: boolean;
} }
export enum TransactionState { export interface TransactionState {
major: TransactionMajorState;
minor?: TransactionMinorState;
}
export enum TransactionMajorState {
// No state, only used when reporting transitions into the initial state // No state, only used when reporting transitions into the initial state
None = "none", None = "none",
Pending = "pending", Pending = "pending",
@ -96,15 +96,13 @@ export enum TransactionState {
Unknown = "unknown", Unknown = "unknown",
} }
export enum TransactionSubstate { export enum TransactionMinorState {
// Placeholder until D37 is fully implemented // Placeholder until D37 is fully implemented
Unknown = "unknown", Unknown = "unknown",
// No substate Deposit = "deposit",
None = "none", KycRequired = "kyc-required",
DepositPendingInitial = "initial", Track = "track",
DepositKycRequired = "kyc-required", Refresh = "refresh",
DepositPendingTrack = "track",
DepositAbortingRefresh = "refresh",
} }
export interface TransactionsResponse { export interface TransactionsResponse {
@ -126,10 +124,11 @@ export interface TransactionCommon {
// main timestamp of the transaction // main timestamp of the transaction
timestamp: TalerProtocolTimestamp; timestamp: TalerProtocolTimestamp;
/**
* Transaction state, as per DD37.
*/
txState: TransactionState; txState: TransactionState;
txSubstate: TransactionSubstate;
/** /**
* @deprecated in favor of statusMajor and statusMinor * @deprecated in favor of statusMajor and statusMinor
*/ */

View File

@ -865,8 +865,10 @@ export enum DepositGroupOperationStatus {
AbortingWithRefresh = 11 /* ACTIVE_START + 1 */, AbortingWithRefresh = 11 /* ACTIVE_START + 1 */,
} }
// FIXME: Improve name! This enum is very specific to deposits. /**
export enum TransactionStatus { * Status of a single element of a deposit group.
*/
export enum DepositElementStatus {
Unknown = 10, Unknown = 10,
Accepted = 20, Accepted = 20,
KycRequired = 30, KycRequired = 30,
@ -1686,7 +1688,7 @@ export interface DepositGroupRecord {
operationStatus: OperationStatus; operationStatus: OperationStatus;
transactionPerCoin: TransactionStatus[]; transactionPerCoin: DepositElementStatus[];
trackingState?: { trackingState?: {
[signature: string]: { [signature: string]: {
@ -2605,7 +2607,7 @@ export const walletDbFixups: FixupDescription[] = [
return; return;
} }
dg.transactionPerCoin = dg.depositedPerCoin.map( dg.transactionPerCoin = dg.depositedPerCoin.map(
(c) => TransactionStatus.Unknown, (c) => DepositElementStatus.Unknown,
); );
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
}); });

View File

@ -109,14 +109,26 @@ export async function checkReserve(
} }
} }
export interface TopupReserveWithDemobankArgs {
http: HttpRequestLibrary;
reservePub: string;
bankBaseUrl: string;
bankAccessApiBaseUrl: string;
exchangeInfo: ExchangeInfo;
amount: AmountString;
}
export async function topupReserveWithDemobank( export async function topupReserveWithDemobank(
http: HttpRequestLibrary, args: TopupReserveWithDemobankArgs,
reservePub: string,
bankBaseUrl: string,
bankAccessApiBaseUrl: string,
exchangeInfo: ExchangeInfo,
amount: AmountString,
) { ) {
const {
bankBaseUrl,
http,
bankAccessApiBaseUrl,
amount,
exchangeInfo,
reservePub,
} = args;
const bankHandle: BankServiceHandle = { const bankHandle: BankServiceHandle = {
baseUrl: bankBaseUrl, baseUrl: bankBaseUrl,
bankAccessApiBaseUrl: bankAccessApiBaseUrl, bankAccessApiBaseUrl: bankAccessApiBaseUrl,

View File

@ -40,6 +40,7 @@ import {
j2s, j2s,
Logger, Logger,
MerchantContractTerms, MerchantContractTerms,
NotificationType,
parsePaytoUri, parsePaytoUri,
PayCoinSelection, PayCoinSelection,
PrepareDepositRequest, PrepareDepositRequest,
@ -49,9 +50,9 @@ import {
TalerErrorCode, TalerErrorCode,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TrackTransaction, TrackTransaction,
TransactionMajorState,
TransactionMinorState,
TransactionState, TransactionState,
TransactionStateInfo,
TransactionSubstate,
TransactionType, TransactionType,
URL, URL,
WireFee, WireFee,
@ -60,13 +61,16 @@ import {
DenominationRecord, DenominationRecord,
DepositGroupRecord, DepositGroupRecord,
OperationStatus, OperationStatus,
TransactionStatus, DepositElementStatus,
} from "../db.js"; } from "../db.js";
import { TalerError } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-util";
import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js"; import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { OperationAttemptResult } from "../util/retries.js"; import {
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
import { spendCoins } from "./common.js"; import { spendCoins } from "./common.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
@ -89,15 +93,13 @@ const logger = new Logger("deposits.ts");
* Get the (DD37-style) transaction status based on the * Get the (DD37-style) transaction status based on the
* database record of a deposit group. * database record of a deposit group.
*/ */
export async function computeDepositTransactionStatus( export function computeDepositTransactionStatus(
ws: InternalWalletState,
dg: DepositGroupRecord, dg: DepositGroupRecord,
): Promise<TransactionStateInfo> { ): TransactionState {
switch (dg.operationStatus) { switch (dg.operationStatus) {
case OperationStatus.Finished: { case OperationStatus.Finished: {
return { return {
txState: TransactionState.Done, major: TransactionMajorState.Done,
txSubstate: TransactionSubstate.None,
}; };
} }
case OperationStatus.Pending: { case OperationStatus.Pending: {
@ -110,10 +112,10 @@ export async function computeDepositTransactionStatus(
numDeposited++; numDeposited++;
} }
switch (dg.transactionPerCoin[i]) { switch (dg.transactionPerCoin[i]) {
case TransactionStatus.KycRequired: case DepositElementStatus.KycRequired:
numKycRequired++; numKycRequired++;
break; break;
case TransactionStatus.Wired: case DepositElementStatus.Wired:
numWired++; numWired++;
break; break;
} }
@ -121,21 +123,21 @@ export async function computeDepositTransactionStatus(
if (numKycRequired > 0) { if (numKycRequired > 0) {
return { return {
txState: TransactionState.Pending, major: TransactionMajorState.Pending,
txSubstate: TransactionSubstate.DepositKycRequired, minor: TransactionMinorState.KycRequired,
}; };
} }
if (numDeposited == numTotal) { if (numDeposited == numTotal) {
return { return {
txState: TransactionState.Pending, major: TransactionMajorState.Pending,
txSubstate: TransactionSubstate.DepositPendingTrack, minor: TransactionMinorState.Track,
}; };
} }
return { return {
txState: TransactionState.Pending, major: TransactionMajorState.Pending,
txSubstate: TransactionSubstate.DepositPendingInitial, minor: TransactionMinorState.Deposit,
}; };
} }
default: default:
@ -221,6 +223,13 @@ export async function processDepositGroup(
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
} }
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
const txStateOld = computeDepositTransactionStatus(depositGroup);
const contractData = extractContractData( const contractData = extractContractData(
depositGroup.contractTermsRaw, depositGroup.contractTermsRaw,
depositGroup.contractTermsHash, depositGroup.contractTermsHash,
@ -239,7 +248,7 @@ export async function processDepositGroup(
for (let i = 0; i < depositPermissions.length; i++) { for (let i = 0; i < depositPermissions.length; i++) {
const perm = depositPermissions[i]; const perm = depositPermissions[i];
let updatedDeposit: boolean | undefined = undefined; let updatedDeposit: boolean = false;
if (!depositGroup.depositedPerCoin[i]) { if (!depositGroup.depositedPerCoin[i]) {
const requestBody: ExchangeDepositRequest = { const requestBody: ExchangeDepositRequest = {
@ -270,7 +279,7 @@ export async function processDepositGroup(
updatedDeposit = true; updatedDeposit = true;
} }
let updatedTxStatus: TransactionStatus | undefined = undefined; let updatedTxStatus: DepositElementStatus | undefined = undefined;
type ValueOf<T> = T[keyof T]; type ValueOf<T> = T[keyof T];
let newWiredTransaction: let newWiredTransaction:
@ -280,12 +289,12 @@ export async function processDepositGroup(
} }
| undefined; | undefined;
if (depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired) { if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDepositPermission(ws, depositGroup, perm); const track = await trackDeposit(ws, depositGroup, perm);
if (track.type === "accepted") { if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) { if (!track.kyc_ok && track.requirement_row !== undefined) {
updatedTxStatus = TransactionStatus.KycRequired; updatedTxStatus = DepositElementStatus.KycRequired;
const { requirement_row: requirementRow } = track; const { requirement_row: requirementRow } = track;
const paytoHash = encodeCrock( const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
@ -297,10 +306,10 @@ export async function processDepositGroup(
"individual", "individual",
); );
} else { } else {
updatedTxStatus = TransactionStatus.Accepted; updatedTxStatus = DepositElementStatus.Accepted;
} }
} else if (track.type === "wired") { } else if (track.type === "wired") {
updatedTxStatus = TransactionStatus.Wired; updatedTxStatus = DepositElementStatus.Wired;
const payto = parsePaytoUri(depositGroup.wire.payto_uri); const payto = parsePaytoUri(depositGroup.wire.payto_uri);
if (!payto) { if (!payto) {
@ -327,11 +336,11 @@ export async function processDepositGroup(
id: track.exchange_sig, id: track.exchange_sig,
}; };
} else { } else {
updatedTxStatus = TransactionStatus.Unknown; updatedTxStatus = DepositElementStatus.Unknown;
} }
} }
if (updatedTxStatus !== undefined || updatedDeposit !== undefined) { if (updatedTxStatus !== undefined || updatedDeposit) {
await ws.db await ws.db
.mktx((x) => [x.depositGroups]) .mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
@ -358,18 +367,18 @@ export async function processDepositGroup(
} }
} }
await ws.db const txStatusNew = await ws.db
.mktx((x) => [x.depositGroups]) .mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId); const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) { if (!dg) {
return; return undefined;
} }
let allDepositedAndWired = true; let allDepositedAndWired = true;
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
if ( if (
!depositGroup.depositedPerCoin[i] || !depositGroup.depositedPerCoin[i] ||
depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
) { ) {
allDepositedAndWired = false; allDepositedAndWired = false;
break; break;
@ -380,8 +389,36 @@ export async function processDepositGroup(
dg.operationStatus = OperationStatus.Finished; dg.operationStatus = OperationStatus.Finished;
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
} }
return computeDepositTransactionStatus(dg);
}); });
if (!txStatusNew) {
// Doesn't exist anymore!
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
}
// Notify if state transitioned
if (
txStateOld.major !== txStatusNew.major ||
txStateOld.minor !== txStatusNew.minor
) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: txStateOld,
newTxState: txStatusNew,
});
}
// FIXME: consider other cases like aborting, suspend, ...
if (
txStatusNew.major === TransactionMajorState.Pending ||
txStatusNew.major === TransactionMajorState.Aborting
) {
return OperationAttemptResult.pendingEmpty();
} else {
return OperationAttemptResult.finishedEmpty();
}
} }
async function getExchangeWireFee( async function getExchangeWireFee(
@ -428,7 +465,7 @@ async function getExchangeWireFee(
return fee; return fee;
} }
async function trackDepositPermission( async function trackDeposit(
ws: InternalWalletState, ws: InternalWalletState,
depositGroup: DepositGroupRecord, depositGroup: DepositGroupRecord,
dp: CoinDepositPermission, dp: CoinDepositPermission,
@ -448,6 +485,7 @@ async function trackDepositPermission(
}); });
url.searchParams.set("merchant_sig", sigResp.sig); url.searchParams.set("merchant_sig", sigResp.sig);
const httpResp = await ws.http.fetch(url.href, { method: "GET" }); const httpResp = await ws.http.fetch(url.href, { method: "GET" });
logger.trace(`deposits response status: ${httpResp.status}`);
switch (httpResp.status) { switch (httpResp.status) {
case HttpStatusCode.Accepted: { case HttpStatusCode.Accepted: {
const accepted = await readSuccessResponseJsonOrThrow( const accepted = await readSuccessResponseJsonOrThrow(
@ -710,7 +748,7 @@ export async function createDepositGroup(
timestampCreated: AbsoluteTime.toTimestamp(now), timestampCreated: AbsoluteTime.toTimestamp(now),
timestampFinished: undefined, timestampFinished: undefined,
transactionPerCoin: payCoinSel.coinSel.coinPubs.map( transactionPerCoin: payCoinSel.coinSel.coinPubs.map(
() => TransactionStatus.Unknown, () => DepositElementStatus.Unknown,
), ),
payCoinSelection: payCoinSel.coinSel, payCoinSelection: payCoinSel.coinSel,
payCoinSelectionUid: encodeCrock(getRandomBytes(32)), payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
@ -733,7 +771,7 @@ export async function createDepositGroup(
depositGroupId, depositGroupId,
}); });
await ws.db const newTxState = await ws.db
.mktx((x) => [ .mktx((x) => [
x.depositGroups, x.depositGroups,
x.coins, x.coins,
@ -752,6 +790,16 @@ export async function createDepositGroup(
refreshReason: RefreshReason.PayDeposit, refreshReason: RefreshReason.PayDeposit,
}); });
await tx.depositGroups.put(depositGroup); await tx.depositGroups.put(depositGroup);
return computeDepositTransactionStatus(depositGroup);
});
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: {
major: TransactionMajorState.None,
},
newTxState,
}); });
return { return {

View File

@ -35,10 +35,9 @@ import {
Transaction, Transaction,
TransactionByIdRequest, TransactionByIdRequest,
TransactionIdStr, TransactionIdStr,
TransactionMajorState,
TransactionsRequest, TransactionsRequest,
TransactionsResponse, TransactionsResponse,
TransactionState,
TransactionSubstate,
TransactionType, TransactionType,
WithdrawalType, WithdrawalType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -58,7 +57,7 @@ import {
WalletContractData, WalletContractData,
PeerPushPaymentInitiationStatus, PeerPushPaymentInitiationStatus,
PeerPullPaymentIncomingStatus, PeerPullPaymentIncomingStatus,
TransactionStatus, DepositElementStatus,
WithdrawalGroupStatus, WithdrawalGroupStatus,
RefreshGroupRecord, RefreshGroupRecord,
RefreshOperationStatus, RefreshOperationStatus,
@ -79,7 +78,10 @@ import {
runOperationWithErrorReporting, runOperationWithErrorReporting,
TombstoneTag, TombstoneTag,
} from "./common.js"; } from "./common.js";
import { processDepositGroup } from "./deposits.js"; import {
computeDepositTransactionStatus,
processDepositGroup,
} from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
abortPay, abortPay,
@ -425,6 +427,11 @@ export async function getTransactionById(
} }
} }
// FIXME: Just a marker helper for unknown states until DD37 is fully implemented.
const mkTxStateUnknown = () => ({
major: TransactionMajorState.Unknown,
});
function buildTransactionForPushPaymentDebit( function buildTransactionForPushPaymentDebit(
pi: PeerPushPaymentInitiationRecord, pi: PeerPushPaymentInitiationRecord,
contractTerms: PeerContractTerms, contractTerms: PeerContractTerms,
@ -432,8 +439,7 @@ function buildTransactionForPushPaymentDebit(
): Transaction { ): Transaction {
return { return {
type: TransactionType.PeerPushDebit, type: TransactionType.PeerPushDebit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: pi.totalCost, amountEffective: pi.totalCost,
amountRaw: pi.amount, amountRaw: pi.amount,
exchangeBaseUrl: pi.exchangeBaseUrl, exchangeBaseUrl: pi.exchangeBaseUrl,
@ -466,8 +472,7 @@ function buildTransactionForPullPaymentDebit(
): Transaction { ): Transaction {
return { return {
type: TransactionType.PeerPullDebit, type: TransactionType.PeerPullDebit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: pi.coinSel?.totalCost amountEffective: pi.coinSel?.totalCost
? pi.coinSel?.totalCost ? pi.coinSel?.totalCost
: Amounts.stringify(pi.contractTerms.amount), : Amounts.stringify(pi.contractTerms.amount),
@ -517,8 +522,7 @@ function buildTransactionForPeerPullCredit(
}); });
return { return {
type: TransactionType.PeerPullCredit, type: TransactionType.PeerPullCredit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount), amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
@ -553,8 +557,7 @@ function buildTransactionForPeerPullCredit(
return { return {
type: TransactionType.PeerPullCredit, type: TransactionType.PeerPullCredit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective), amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective),
amountRaw: Amounts.stringify(peerContractTerms.amount), amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pullCredit.exchangeBaseUrl, exchangeBaseUrl: pullCredit.exchangeBaseUrl,
@ -593,8 +596,7 @@ function buildTransactionForPeerPushCredit(
return { return {
type: TransactionType.PeerPushCredit, type: TransactionType.PeerPushCredit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount), amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
@ -618,8 +620,7 @@ function buildTransactionForPeerPushCredit(
return { return {
type: TransactionType.PeerPushCredit, type: TransactionType.PeerPushCredit,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
// FIXME: This is wrong, needs to consider fees! // FIXME: This is wrong, needs to consider fees!
amountEffective: Amounts.stringify(peerContractTerms.amount), amountEffective: Amounts.stringify(peerContractTerms.amount),
amountRaw: Amounts.stringify(peerContractTerms.amount), amountRaw: Amounts.stringify(peerContractTerms.amount),
@ -649,8 +650,7 @@ function buildTransactionForBankIntegratedWithdraw(
return { return {
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount), amountRaw: Amounts.stringify(wsr.instructedAmount),
withdrawalDetails: { withdrawalDetails: {
@ -696,8 +696,7 @@ function buildTransactionForManualWithdraw(
return { return {
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify( amountEffective: Amounts.stringify(
withdrawalGroup.denomsSel.totalCoinValue, withdrawalGroup.denomsSel.totalCoinValue,
), ),
@ -748,8 +747,7 @@ function buildTransactionForRefresh(
).amount; ).amount;
return { return {
type: TransactionType.Refresh, type: TransactionType.Refresh,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
refreshReason: refreshGroupRecord.reason, refreshReason: refreshGroupRecord.reason,
amountEffective: Amounts.stringify( amountEffective: Amounts.stringify(
Amounts.zeroOfCurrency(refreshGroupRecord.currency), Amounts.zeroOfCurrency(refreshGroupRecord.currency),
@ -791,8 +789,7 @@ function buildTransactionForDeposit(
return { return {
type: TransactionType.Deposit, type: TransactionType.Deposit,
txState: TransactionState.Unknown, txState: computeDepositTransactionStatus(dg),
txSubstate: TransactionSubstate.Unknown,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount), amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost), amountEffective: Amounts.stringify(dg.totalPayCost),
extendedStatus: dg.timestampFinished extendedStatus: dg.timestampFinished
@ -810,7 +807,7 @@ function buildTransactionForDeposit(
wireTransferProgress: wireTransferProgress:
(100 * (100 *
dg.transactionPerCoin.reduce( dg.transactionPerCoin.reduce(
(prev, cur) => prev + (cur === TransactionStatus.Wired ? 1 : 0), (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
0, 0,
)) / )) /
dg.transactionPerCoin.length, dg.transactionPerCoin.length,
@ -829,8 +826,7 @@ function buildTransactionForTip(
return { return {
type: TransactionType.Tip, type: TransactionType.Tip,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
extendedStatus: tipRecord.pickedUpTimestamp extendedStatus: tipRecord.pickedUpTimestamp
@ -926,8 +922,7 @@ async function buildTransactionForRefund(
return { return {
type: TransactionType.Refund, type: TransactionType.Refund,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
info, info,
refundedTransactionId: makeTransactionId( refundedTransactionId: makeTransactionId(
TransactionType.Payment, TransactionType.Payment,
@ -1030,8 +1025,7 @@ async function buildTransactionForPurchase(
return { return {
type: TransactionType.Payment, type: TransactionType.Payment,
txState: TransactionState.Unknown, txState: mkTxStateUnknown(),
txSubstate: TransactionSubstate.Unknown,
amountRaw: Amounts.stringify(contractData.amount), amountRaw: Amounts.stringify(contractData.amount),
amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost), amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
totalRefundRaw: Amounts.stringify(totalRefund.raw), totalRefundRaw: Amounts.stringify(totalRefund.raw),

View File

@ -70,6 +70,12 @@ export namespace OperationAttemptResult {
result: undefined, result: undefined,
}; };
} }
export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
} }
export interface OperationAttemptFinishedResult<T> { export interface OperationAttemptFinishedResult<T> {