diff options
-rw-r--r-- | packages/taler-harness/src/index.ts | 31 | ||||
-rw-r--r-- | packages/taler-util/src/taler-crypto.ts | 16 | ||||
-rw-r--r-- | packages/taler-wallet-cli/src/index.ts | 38 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 27 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoTypes.ts | 15 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 26 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 513 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/transactions.ts | 26 |
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. |