aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-harness/src/index.ts31
-rw-r--r--packages/taler-util/src/taler-crypto.ts16
-rw-r--r--packages/taler-wallet-cli/src/index.ts38
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts27
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts15
-rw-r--r--packages/taler-wallet-core/src/db.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts513
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts26
8 files changed, 511 insertions, 181 deletions
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index e8eb57fe9..30b557986 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -267,7 +267,7 @@ deploymentCli
});
deploymentCli
- .subcommand("testTalerdotnetDemo", "test-demo-talerdotnet")
+ .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet")
.action(async (args) => {
const http = createPlatformHttpLib();
const cryptiDisp = new CryptoDispatcher(
@@ -296,6 +296,35 @@ deploymentCli
});
deploymentCli
+ .subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet")
+ .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.test.taler.net/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithDemobank({
+ amount: "TESTKUDOS:10",
+ bankAccessApiBaseUrl:
+ "https://bank.test.taler.net/demobanks/default/access-api/",
+ 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
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index db64efcf2..fa92f5683 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -868,6 +868,21 @@ export function bufferForUint32(n: number): Uint8Array {
return buf;
}
+/**
+ * This makes the assumption that the uint64 fits a float,
+ * which should be true for all Taler protocol messages.
+ */
+export function bufferForUint64(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(4);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ if (n < 0 || !Number.isInteger(n)) {
+ throw Error("non-negative integer expected");
+ }
+ dv.setBigUint64(0, BigInt(n));
+ return buf;
+}
+
export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf);
@@ -933,6 +948,7 @@ export enum TalerSignaturePurpose {
TEST = 4242,
MERCHANT_PAYMENT_OK = 1104,
MERCHANT_CONTRACT = 1101,
+ MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index cc7c119b9..a31aec2eb 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -386,8 +386,12 @@ walletCli
const transactionsCli = walletCli
.subcommand("transactions", "transactions", { help: "Manage transactions." })
- .maybeOption("currency", ["--currency"], clk.STRING)
- .maybeOption("search", ["--search"], clk.STRING)
+ .maybeOption("currency", ["--currency"], clk.STRING, {
+ help: "Filter by currency.",
+ })
+ .maybeOption("search", ["--search"], clk.STRING, {
+ help: "Filter by search string",
+ })
.flag("includeRefreshes", ["--include-refreshes"]);
// Default action
@@ -421,6 +425,36 @@ transactionsCli
});
transactionsCli
+ .subcommand("suspendTransaction", "suspend", {
+ help: "Suspend a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to suspend.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.SuspendTransaction, {
+ transactionId: args.suspendTransaction.transactionId,
+ });
+ });
+ });
+
+transactionsCli
+ .subcommand("resumeTransaction", "resume", {
+ help: "Resume a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to suspend.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.ResumeTransaction, {
+ transactionId: args.resumeTransaction.transactionId,
+ });
+ });
+ });
+
+transactionsCli
.subcommand("lookup", "lookup", {
help: "Look up a single transaction based on the transaction identifier.",
})
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 52d2dd24e..fa1271a7b 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -33,6 +33,7 @@ import {
AmountString,
BlindedDenominationSignature,
bufferForUint32,
+ bufferForUint64,
buildSigPS,
CoinDepositPermission,
CoinEnvelope,
@@ -105,6 +106,8 @@ import {
EncryptedContract,
SignPurseMergeRequest,
SignPurseMergeResponse,
+ SignRefundRequest,
+ SignRefundResponse,
SignReservePurseCreateRequest,
SignReservePurseCreateResponse,
SignTrackTransactionRequest,
@@ -233,6 +236,8 @@ export interface TalerCryptoInterface {
signReservePurseCreate(
req: SignReservePurseCreateRequest,
): Promise<SignReservePurseCreateResponse>;
+
+ signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
}
/**
@@ -409,6 +414,9 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignReservePurseCreateResponse> {
throw new Error("Function not implemented.");
},
+ signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> {
+ throw new Error("Function not implemented.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -928,6 +936,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const pub = decodeCrock(masterPub);
return { valid: eddsaVerify(p, sig, pub) };
},
+
/**
* Check if the signature of a denomination is valid.
*/
@@ -1625,6 +1634,24 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
purseSig: purseSigResp.sig,
};
},
+ async signRefund(
+ tci: TalerCryptoInterfaceR,
+ req: SignRefundRequest,
+ ): Promise<SignRefundResponse> {
+ const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND)
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(req.coinPub))
+ .put(bufferForUint64(req.rtransactionId))
+ .put(amountToBuffer(req.refundAmount))
+ .build();
+ const refundSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(refundSigBlob),
+ priv: req.merchantPriv,
+ });
+ return {
+ sig: refundSigResp.sig,
+ };
+ },
};
function amountToBuffer(amount: AmountLike): Uint8Array {
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index 98f3c935b..3b27db0c0 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -255,6 +255,21 @@ export interface SignPurseMergeResponse {
accountSig: string;
}
+export interface SignRefundRequest {
+ merchantPriv: string;
+ merchantPub: string;
+ contractTermsHash: string;
+ coinPub: string;
+ rtransactionId: number;
+ refundAmount: AmountString;
+}
+
+export interface SignRefundResponse {
+ sig: string;
+}
+
+export interface SignRefundResponse {}
+
export interface SignReservePurseCreateRequest {
mergeTimestamp: TalerProtocolTimestamp;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 0bfe11aaa..a8c103265 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -873,6 +873,8 @@ export enum DepositElementStatus {
Accepted = 20,
KycRequired = 30,
Wired = 40,
+ RefundSuccess = 50,
+ RefundFailed = 51,
}
/**
@@ -1639,6 +1641,14 @@ export interface BackupProviderRecord {
uids: string[];
}
+export enum DepositOperationStatus {
+ Finished = 50 /* OperationStatusRange.DORMANT_START */,
+ Suspended = 51 /* OperationStatusRange.DORMANT_START + 1 */,
+ Aborted = 52 /* OperationStatusRange.DORMANT_START + 2 */,
+ Pending = 10 /* OperationStatusRange.ACTIVE_START */,
+ Aborting = 11 /* OperationStatusRange.ACTIVE_START + 1 */,
+}
+
/**
* Group of deposits made by the wallet.
*/
@@ -1680,16 +1690,26 @@ export interface DepositGroupRecord {
*/
effectiveDepositAmount: AmountString;
- depositedPerCoin: boolean[];
-
timestampCreated: TalerProtocolTimestamp;
timestampFinished: TalerProtocolTimestamp | undefined;
- operationStatus: OperationStatus;
+ operationStatus: DepositOperationStatus;
+ // FIXME: Duplication between this and transactionPerCoin!
+ depositedPerCoin: boolean[];
+
+ // FIXME: Improve name!
transactionPerCoin: DepositElementStatus[];
+ /**
+ * When the deposit transaction was aborted and
+ * refreshes were tried, we create a refresh
+ * group and store the ID here.
+ */
+ abortRefreshGroupId?: string;
+
+ // FIXME: Do we need this and should it be in this object store?
trackingState?: {
[signature: string]: {
// Raw wire transfer identifier of the deposit.
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 6e56b0897..051cbc176 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -64,13 +64,16 @@ import {
DepositElementStatus,
} from "../db.js";
import { TalerError } from "@gnu-taler/taler-util";
-import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js";
+import {
+ DepositOperationStatus,
+ getTotalRefreshCost,
+ KycPendingInfo,
+ KycUserType,
+ PendingTaskType,
+} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
+import { OperationAttemptResult } from "../util/retries.js";
import { spendCoins } from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import {
@@ -82,7 +85,9 @@ import { selectPayCoinsNew } from "../util/coinSelection.js";
import {
constructTransactionIdentifier,
parseTransactionIdentifier,
+ stopLongpolling,
} from "./transactions.js";
+import { constructTaskIdentifier } from "../util/retries.js";
/**
* Logger.
@@ -97,12 +102,12 @@ export function computeDepositTransactionStatus(
dg: DepositGroupRecord,
): TransactionState {
switch (dg.operationStatus) {
- case OperationStatus.Finished: {
+ case DepositOperationStatus.Finished: {
return {
major: TransactionMajorState.Done,
};
}
- case OperationStatus.Pending: {
+ case DepositOperationStatus.Pending: {
const numTotal = dg.payCoinSelection.coinPubs.length;
let numDeposited = 0;
let numKycRequired = 0;
@@ -140,6 +145,10 @@ export function computeDepositTransactionStatus(
minor: TransactionMinorState.Deposit,
};
}
+ case DepositOperationStatus.Suspended:
+ return {
+ major: TransactionMajorState.Suspended,
+ };
default:
throw Error("unexpected deposit group state");
}
@@ -149,13 +158,156 @@ export async function suspendDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
- throw Error("not implemented");
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.Pending: {
+ dg.operationStatus = DepositOperationStatus.Suspended;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ case DepositOperationStatus.Suspended:
+ return undefined;
+ }
+ return undefined;
+ });
+ stopLongpolling(ws, retryTag);
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
+}
+
+export async function resumeDepositGroup(
+ ws: InternalWalletState,
+ depositGroupId: string,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return;
+ case DepositOperationStatus.Pending: {
+ return;
+ }
+ case DepositOperationStatus.Suspended:
+ dg.operationStatus = DepositOperationStatus.Pending;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ return undefined;
+ });
+ ws.latch.trigger();
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
}
export async function abortDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.Pending: {
+ dg.operationStatus = DepositOperationStatus.Aborting;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ case DepositOperationStatus.Suspended:
+ // FIXME: Can we abort a suspended transaction?!
+ return undefined;
+ }
+ return undefined;
+ });
+ stopLongpolling(ws, retryTag);
+ // Need to process the operation again.
+ ws.latch.trigger();
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
+}
+
+export async function deleteDepositGroup(
+ ws: InternalWalletState,
+ depositGroupId: boolean,
+ opts: { forced?: boolean } = {},
+) {
throw Error("not implemented");
}
@@ -230,195 +382,210 @@ export async function processDepositGroup(
const txStateOld = computeDepositTransactionStatus(depositGroup);
- const contractData = extractContractData(
- depositGroup.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
- );
+ if (depositGroup.operationStatus === DepositOperationStatus.Pending) {
+ const contractData = extractContractData(
+ depositGroup.contractTermsRaw,
+ depositGroup.contractTermsHash,
+ "",
+ );
- // Check for cancellation before expensive operations.
- options.cancellationToken?.throwIfCancelled();
- // FIXME: Cache these!
- const depositPermissions = await generateDepositPermissions(
- ws,
- depositGroup.payCoinSelection,
- contractData,
- );
+ // Check for cancellation before expensive operations.
+ options.cancellationToken?.throwIfCancelled();
+ // FIXME: Cache these!
+ const depositPermissions = await generateDepositPermissions(
+ ws,
+ depositGroup.payCoinSelection,
+ contractData,
+ );
- for (let i = 0; i < depositPermissions.length; i++) {
- const perm = depositPermissions[i];
-
- let updatedDeposit: boolean = false;
-
- if (!depositGroup.depositedPerCoin[i]) {
- const requestBody: ExchangeDepositRequest = {
- contribution: Amounts.stringify(perm.contribution),
- merchant_payto_uri: depositGroup.wire.payto_uri,
- wire_salt: depositGroup.wire.salt,
- h_contract_terms: depositGroup.contractTermsHash,
- ub_sig: perm.ub_sig,
- timestamp: depositGroup.contractTermsRaw.timestamp,
- wire_transfer_deadline:
- depositGroup.contractTermsRaw.wire_transfer_deadline,
- refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
- coin_sig: perm.coin_sig,
- denom_pub_hash: perm.h_denom,
- merchant_pub: depositGroup.merchantPub,
- h_age_commitment: perm.h_age_commitment,
- };
- // Check for cancellation before making network request.
- options.cancellationToken?.throwIfCancelled();
- const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
- logger.info(`depositing to ${url}`);
- const httpResp = await ws.http.fetch(url.href, {
- method: "POST",
- body: requestBody,
- cancellationToken: options.cancellationToken,
- });
- await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
- updatedDeposit = true;
- }
+ for (let i = 0; i < depositPermissions.length; i++) {
+ const perm = depositPermissions[i];
+
+ let updatedDeposit: boolean = false;
+
+ if (!depositGroup.depositedPerCoin[i]) {
+ const requestBody: ExchangeDepositRequest = {
+ contribution: Amounts.stringify(perm.contribution),
+ merchant_payto_uri: depositGroup.wire.payto_uri,
+ wire_salt: depositGroup.wire.salt,
+ h_contract_terms: depositGroup.contractTermsHash,
+ ub_sig: perm.ub_sig,
+ timestamp: depositGroup.contractTermsRaw.timestamp,
+ wire_transfer_deadline:
+ depositGroup.contractTermsRaw.wire_transfer_deadline,
+ refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
+ coin_sig: perm.coin_sig,
+ denom_pub_hash: perm.h_denom,
+ merchant_pub: depositGroup.merchantPub,
+ h_age_commitment: perm.h_age_commitment,
+ };
+ // Check for cancellation before making network request.
+ options.cancellationToken?.throwIfCancelled();
+ const url = new URL(
+ `coins/${perm.coin_pub}/deposit`,
+ perm.exchange_url,
+ );
+ logger.info(`depositing to ${url}`);
+ const httpResp = await ws.http.fetch(url.href, {
+ method: "POST",
+ body: requestBody,
+ cancellationToken: options.cancellationToken,
+ });
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForDepositSuccess(),
+ );
+ updatedDeposit = true;
+ }
- let updatedTxStatus: DepositElementStatus | undefined = undefined;
- type ValueOf<T> = T[keyof T];
+ let updatedTxStatus: DepositElementStatus | undefined = undefined;
+ type ValueOf<T> = T[keyof T];
- let newWiredTransaction:
- | {
- id: string;
- value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
- }
- | undefined;
+ let newWiredTransaction:
+ | {
+ id: string;
+ value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
+ }
+ | undefined;
- if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
- const track = await trackDeposit(ws, depositGroup, perm);
+ if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
+ const track = await trackDeposit(ws, depositGroup, perm);
- if (track.type === "accepted") {
- if (!track.kyc_ok && track.requirement_row !== undefined) {
- updatedTxStatus = DepositElementStatus.KycRequired;
- const { requirement_row: requirementRow } = track;
- const paytoHash = encodeCrock(
- hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
- );
- await checkDepositKycStatus(
+ if (track.type === "accepted") {
+ if (!track.kyc_ok && track.requirement_row !== undefined) {
+ updatedTxStatus = DepositElementStatus.KycRequired;
+ const { requirement_row: requirementRow } = track;
+ const paytoHash = encodeCrock(
+ hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
+ );
+ await checkDepositKycStatus(
+ ws,
+ perm.exchange_url,
+ { paytoHash, requirementRow },
+ "individual",
+ );
+ } else {
+ updatedTxStatus = DepositElementStatus.Accepted;
+ }
+ } else if (track.type === "wired") {
+ updatedTxStatus = DepositElementStatus.Wired;
+
+ const payto = parsePaytoUri(depositGroup.wire.payto_uri);
+ if (!payto) {
+ throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
+ }
+
+ const fee = await getExchangeWireFee(
ws,
+ payto.targetType,
perm.exchange_url,
- { paytoHash, requirementRow },
- "individual",
+ track.execution_time,
);
+ const raw = Amounts.parseOrThrow(track.coin_contribution);
+ const wireFee = Amounts.parseOrThrow(fee.wireFee);
+
+ newWiredTransaction = {
+ value: {
+ amountRaw: Amounts.stringify(raw),
+ wireFee: Amounts.stringify(wireFee),
+ exchangePub: track.exchange_pub,
+ timestampExecuted: track.execution_time,
+ wireTransferId: track.wtid,
+ },
+ id: track.exchange_sig,
+ };
} else {
- updatedTxStatus = DepositElementStatus.Accepted;
+ updatedTxStatus = DepositElementStatus.Unknown;
}
- } else if (track.type === "wired") {
- updatedTxStatus = DepositElementStatus.Wired;
+ }
- const payto = parsePaytoUri(depositGroup.wire.payto_uri);
- if (!payto) {
- throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
- }
+ if (updatedTxStatus !== undefined || updatedDeposit) {
+ await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
+ }
+ if (updatedDeposit !== undefined) {
+ dg.depositedPerCoin[i] = updatedDeposit;
+ }
+ if (updatedTxStatus !== undefined) {
+ dg.transactionPerCoin[i] = updatedTxStatus;
+ }
+ if (newWiredTransaction) {
+ if (!dg.trackingState) {
+ dg.trackingState = {};
+ }
- const fee = await getExchangeWireFee(
- ws,
- payto.targetType,
- perm.exchange_url,
- track.execution_time,
- );
- const raw = Amounts.parseOrThrow(track.coin_contribution);
- const wireFee = Amounts.parseOrThrow(fee.wireFee);
-
- newWiredTransaction = {
- value: {
- amountRaw: Amounts.stringify(raw),
- wireFee: Amounts.stringify(wireFee),
- exchangePub: track.exchange_pub,
- timestampExecuted: track.execution_time,
- wireTransferId: track.wtid,
- },
- id: track.exchange_sig,
- };
- } else {
- updatedTxStatus = DepositElementStatus.Unknown;
+ dg.trackingState[newWiredTransaction.id] =
+ newWiredTransaction.value;
+ }
+ await tx.depositGroups.put(dg);
+ });
}
}
- if (updatedTxStatus !== undefined || updatedDeposit) {
- await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- if (updatedDeposit !== undefined) {
- dg.depositedPerCoin[i] = updatedDeposit;
- }
- if (updatedTxStatus !== undefined) {
- dg.transactionPerCoin[i] = updatedTxStatus;
- }
- if (newWiredTransaction) {
- if (!dg.trackingState) {
- dg.trackingState = {};
- }
-
- dg.trackingState[newWiredTransaction.id] =
- newWiredTransaction.value;
+ const txStatusNew = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ let allDepositedAndWired = true;
+ for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
+ if (
+ !depositGroup.depositedPerCoin[i] ||
+ depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
+ ) {
+ allDepositedAndWired = false;
+ break;
}
+ }
+ if (allDepositedAndWired) {
+ dg.timestampFinished = TalerProtocolTimestamp.now();
+ dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg);
- });
- }
- }
-
- const txStatusNew = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return undefined;
- }
- let allDepositedAndWired = true;
- for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
- if (
- !depositGroup.depositedPerCoin[i] ||
- depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
- ) {
- allDepositedAndWired = false;
- break;
}
- }
- if (allDepositedAndWired) {
- dg.timestampFinished = TalerProtocolTimestamp.now();
- dg.operationStatus = OperationStatus.Finished;
- await tx.depositGroups.put(dg);
- }
- return computeDepositTransactionStatus(dg);
- });
+ return computeDepositTransactionStatus(dg);
+ });
- if (!txStatusNew) {
- // Doesn't exist anymore!
- return OperationAttemptResult.finishedEmpty();
- }
+ if (!txStatusNew) {
+ // Doesn't exist anymore!
+ 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,
- });
+ // 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();
+ }
}
- // FIXME: consider other cases like aborting, suspend, ...
- if (
- txStatusNew.major === TransactionMajorState.Pending ||
- txStatusNew.major === TransactionMajorState.Aborting
- ) {
+ if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
+ // FIXME: Implement!
return OperationAttemptResult.pendingEmpty();
- } else {
- return OperationAttemptResult.finishedEmpty();
}
+
+ return OperationAttemptResult.finishedEmpty();
}
async function getExchangeWireFee(
@@ -763,7 +930,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri,
salt: wireSalt,
},
- operationStatus: OperationStatus.Pending,
+ operationStatus: DepositOperationStatus.Pending,
};
const transactionId = constructTransactionIdentifier({
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 884844ba6..1a511583a 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -81,6 +81,7 @@ import {
import {
computeDepositTransactionStatus,
processDepositGroup,
+ suspendDepositGroup,
} from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js";
import {
@@ -1615,7 +1616,19 @@ export async function retryTransaction(
export async function suspendTransaction(
ws: InternalWalletState,
transactionId: string,
-): Promise<void> {}
+): Promise<void> {
+ const tx = parseTransactionIdentifier(transactionId);
+ if (!tx) {
+ throw Error("invalid transaction ID");
+ }
+ switch (tx.tag) {
+ case TransactionType.Deposit:
+ await suspendDepositGroup(ws, tx.depositGroupId);
+ return;
+ default:
+ logger.warn(`unable to suspend transaction of type '${tx.tag}'`);
+ }
+}
/**
* Resume a suspended transaction.
@@ -1623,7 +1636,16 @@ export async function suspendTransaction(
export async function resumeTransaction(
ws: InternalWalletState,
transactionId: string,
-): Promise<void> {}
+): Promise<void> {
+ const tx = parseTransactionIdentifier(transactionId);
+ if (!tx) {
+ throw Error("invalid transaction ID");
+ }
+ switch (tx.tag) {
+ default:
+ logger.warn(`unable to resume transaction of type '${tx.tag}'`);
+ }
+}
/**
* Permanently delete a transaction based on the transaction ID.