diff options
7 files changed, 554 insertions, 120 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts new file mode 100644 index 000000000..6de3c2e33 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts @@ -0,0 +1,194 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + AmountString, + Duration, + j2s, + NotificationType, + TransactionMajorState, + TransactionMinorState, + TransactionType, + WalletNotification, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { CoinConfig } from "../harness/denomStructures.js"; +import { + BankServiceHandle, + ExchangeService, + GlobalTestState, + WalletClient, +} from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + withdrawViaBankV2, +} from "../harness/helpers.js"; + +const coinCommon = { + cipher: "RSA" as const, + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + feeDeposit: "TESTKUDOS:0", + feeRefresh: "TESTKUDOS:0", + feeRefund: "TESTKUDOS:0", + feeWithdraw: "TESTKUDOS:0", + rsaKeySize: 1024, +}; + +const coinConfigList: CoinConfig[] = [ + { + ...coinCommon, + name: "n1", + value: "TESTKUDOS:1", + }, +]; + +export async function runPeerPullLargeTest(t: GlobalTestState) { + // Set up test environment + + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2( + t, + coinConfigList, + ); + + let allW1Notifications: WalletNotification[] = []; + let allW2Notifications: WalletNotification[] = []; + + const w1 = await createWalletDaemonWithClient(t, { + name: "w1", + persistent: true, + handleNotification(wn) { + allW1Notifications.push(wn); + }, + }); + const w2 = await createWalletDaemonWithClient(t, { + name: "w2", + persistent: true, + handleNotification(wn) { + allW2Notifications.push(wn); + }, + }); + + // Withdraw digital cash into the wallet. + const wallet1 = w1.walletClient; + const wallet2 = w2.walletClient; + + await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2); +} + +async function checkNormalPeerPull( + t: GlobalTestState, + bank: BankServiceHandle, + exchange: ExchangeService, + wallet1: WalletClient, + wallet2: WalletClient, +): Promise<void> { + const withdrawRes = await withdrawViaBankV2(t, { + walletClient: wallet2, + bank, + exchange, + amount: "TESTKUDOS:500", + }); + + await withdrawRes.withdrawalFinishedCond; + + const purseExpiration = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + + const resp = await wallet1.client.call( + WalletApiOperation.InitiatePeerPullCredit, + { + exchangeBaseUrl: exchange.baseUrl, + partialContractTerms: { + summary: "Hello World", + amount: "TESTKUDOS:200" as AmountString, + purse_expiration: purseExpiration, + }, + }, + ); + + const peerPullCreditReadyCond = wallet1.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.transactionId === resp.transactionId && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.Ready, + ); + + await peerPullCreditReadyCond; + + const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, { + transactionId: resp.transactionId, + }); + + t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit); + t.assertTrue(!!creditTx.talerUri); + + const checkResp = await wallet2.client.call( + WalletApiOperation.PreparePeerPullDebit, + { + talerUri: creditTx.talerUri, + }, + ); + + console.log(`checkResp: ${j2s(checkResp)}`); + + const peerPullCreditDoneCond = wallet1.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.transactionId === resp.transactionId && + x.newTxState.major === TransactionMajorState.Done, + ); + + const peerPullDebitDoneCond = wallet2.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.transactionId === checkResp.transactionId && + x.newTxState.major === TransactionMajorState.Done, + ); + + await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, { + transactionId: checkResp.transactionId, + }); + + await peerPullCreditDoneCond; + await peerPullDebitDoneCond; + + const txn1 = await wallet1.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + + const txn2 = await wallet2.client.call( + WalletApiOperation.GetTransactions, + {}, + ); + + console.log(`txn1: ${j2s(txn1)}`); + console.log(`txn2: ${j2s(txn2)}`); +} + +runPeerPullLargeTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-peer-push-large.ts b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts new file mode 100644 index 000000000..b7fbe9f6e --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts @@ -0,0 +1,177 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + AmountString, + Duration, + NotificationType, + TransactionMajorState, + TransactionMinorState, + TransactionType, + WalletNotification, + j2s, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, + withdrawViaBankV2, +} from "../harness/helpers.js"; +import { CoinConfig } from "../harness/denomStructures.js"; + +const coinCommon = { + cipher: "RSA" as const, + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + feeDeposit: "TESTKUDOS:0", + feeRefresh: "TESTKUDOS:0", + feeRefund: "TESTKUDOS:0", + feeWithdraw: "TESTKUDOS:0", + rsaKeySize: 1024, +}; + +const coinConfigList: CoinConfig[] = [ + { + ...coinCommon, + name: "n1", + value: "TESTKUDOS:1", + }, +]; + +/** + * Run a test for a multi-batch peer push payment. + */ +export async function runPeerPushLargeTest(t: GlobalTestState) { + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t, coinConfigList); + + let allW1Notifications: WalletNotification[] = []; + let allW2Notifications: WalletNotification[] = []; + + const w1 = await createWalletDaemonWithClient(t, { + name: "w1", + handleNotification(wn) { + allW1Notifications.push(wn); + }, + }); + const w2 = await createWalletDaemonWithClient(t, { + name: "w2", + handleNotification(wn) { + allW2Notifications.push(wn); + }, + }); + + // Withdraw digital cash into the wallet. + + const withdrawRes = await withdrawViaBankV2(t, { + walletClient: w1.walletClient, + bank, + exchange, + amount: "TESTKUDOS:300", + }); + + await withdrawRes.withdrawalFinishedCond; + + const purse_expiration = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + + const checkResp0 = await w1.walletClient.call( + WalletApiOperation.CheckPeerPushDebit, + { + amount: "TESTKUDOS:200" as AmountString, + }, + ); + + t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:200"); + + const resp = await w1.walletClient.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + summary: "Hello World 🥺", + amount: "TESTKUDOS:200" as AmountString, + purse_expiration, + }, + }, + ); + + console.log(resp); + + const peerPushReadyCond = w1.walletClient.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.Ready && + x.transactionId === resp.transactionId, + ); + + await peerPushReadyCond; + + const txDetails = await w1.walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: resp.transactionId, + }, + ); + t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit); + t.assertTrue(!!txDetails.talerUri); + + const checkResp = await w2.walletClient.call( + WalletApiOperation.PreparePeerPushCredit, + { + talerUri: txDetails.talerUri, + }, + ); + + console.log(checkResp); + + const acceptResp = await w2.walletClient.call( + WalletApiOperation.ConfirmPeerPushCredit, + { + transactionId: checkResp.transactionId, + }, + ); + + console.log(acceptResp); + + await w2.walletClient.call( + WalletApiOperation.TestingWaitTransactionsFinal, + {}, + ); + + const txn1 = await w1.walletClient.call( + WalletApiOperation.GetTransactions, + {}, + ); + const txn2 = await w2.walletClient.call( + WalletApiOperation.GetTransactions, + {}, + ); + + console.log(`txn1: ${j2s(txn1)}`); + console.log(`txn2: ${j2s(txn2)}`); +} + +runPeerPushLargeTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts index 15167d133..004de87c8 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts @@ -44,25 +44,25 @@ const coinCommon = { rsaKeySize: 1024, }; +const coinConfigList: CoinConfig[] = [ + { + ...coinCommon, + name: "n1", + value: "TESTKUDOS:1", + }, + { + ...coinCommon, + name: "n5", + value: "TESTKUDOS:5", + }, +]; + /** * Run test for paying a merchant with balance locked behind a pending refresh. */ export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) { // Set up test environment - const coinConfigList: CoinConfig[] = [ - { - ...coinCommon, - name: "n1", - value: "TESTKUDOS:1", - }, - { - ...coinCommon, - name: "n5", - value: "TESTKUDOS:5", - }, - ]; - const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2( t, coinConfigList, diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 54c211c6b..2f6304773 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -72,6 +72,8 @@ import { runPaymentTransientTest } from "./test-payment-transient.js"; import { runPaymentZeroTest } from "./test-payment-zero.js"; import { runPaymentTest } from "./test-payment.js"; import { runPaywallFlowTest } from "./test-paywall-flow.js"; +import { runPeerPullLargeTest } from "./test-peer-pull-large.js"; +import { runPeerPushLargeTest } from "./test-peer-push-large.js"; import { runPeerRepairTest } from "./test-peer-repair.js"; import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js"; import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js"; @@ -224,6 +226,8 @@ const allTests: TestMainFunction[] = [ runWalletBlockedPayPeerPullTest, runWalletExchangeUpdateTest, runWalletRefreshErrorsTest, + runPeerPullLargeTest, + runPeerPushLargeTest, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 77ee65e52..0745d70c4 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -1468,15 +1468,12 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0")); const deposits: PurseDeposit[] = []; for (const c of req.coins) { - let haveAch: boolean; let maybeAch: Uint8Array; if (c.ageCommitmentProof) { - haveAch = true; maybeAch = decodeCrock( AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment), ); } else { - haveAch = false; maybeAch = new Uint8Array(32); } const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT) diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts index 705317eb6..92eb44a87 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -33,6 +33,7 @@ import { HttpStatusCode, Logger, NotificationType, + ObservabilityEventType, PeerContractTerms, PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, @@ -425,6 +426,11 @@ async function processPeerPullDebitPendingDeposit( wex: WalletExecutionContext, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise<TaskRunResult> { + const ctx = new PeerPullDebitTransactionContext( + wex, + peerPullInc.peerPullDebitId, + ); + const pursePub = peerPullInc.pursePub; const coinSel = peerPullInc.coinSel; @@ -512,70 +518,82 @@ async function processPeerPullDebitPendingDeposit( } } - const coins = await queryCoinInfosForSelection(wex, coinSel); - - const depositSigsResp = await wex.cryptoApi.signPurseDeposits({ - exchangeBaseUrl: peerPullInc.exchangeBaseUrl, - pursePub: peerPullInc.pursePub, - coins, - }); - const purseDepositUrl = new URL( `purses/${pursePub}/deposit`, peerPullInc.exchangeBaseUrl, ); - const depositPayload: ExchangePurseDeposits = { - deposits: depositSigsResp.deposits, - }; + // FIXME: We could skip batches that we've already submitted. - if (logger.shouldLogTrace()) { - logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); - } + const coins = await queryCoinInfosForSelection(wex, coinSel); - const httpResp = await wex.http.fetch(purseDepositUrl.href, { - method: "POST", - body: depositPayload, - cancellationToken: wex.cancellationToken, - }); + const maxBatchSize = 100; - const ctx = new PeerPullDebitTransactionContext( - wex, - peerPullInc.peerPullDebitId, - ); + for (let i = 0; i < coins.length; i += maxBatchSize) { + const batchSize = Math.min(maxBatchSize, coins.length - i); - switch (httpResp.status) { - case HttpStatusCode.Ok: { - const resp = await readSuccessResponseJsonOrThrow( - httpResp, - codecForAny(), - ); - logger.trace(`purse deposit response: ${j2s(resp)}`); + wex.oc.observe({ + type: ObservabilityEventType.Message, + contents: `Depositing batch at ${i}/${coins.length} of size ${batchSize}`, + }); - await ctx.transition(async (r) => { - if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) { - return TransitionResultType.Stay; - } - r.status = PeerPullDebitRecordStatus.Done; - return TransitionResultType.Transition; - }); - return TaskRunResult.finished(); - } - case HttpStatusCode.Gone: { - await ctx.abortTransaction(); - return TaskRunResult.backoff(); - } - case HttpStatusCode.Conflict: { - return handlePurseCreationConflict(ctx, peerPullInc, httpResp); + const batchCoins = coins.slice(i, i + batchSize); + const depositSigsResp = await wex.cryptoApi.signPurseDeposits({ + exchangeBaseUrl: peerPullInc.exchangeBaseUrl, + pursePub: peerPullInc.pursePub, + coins: batchCoins, + }); + + const depositPayload: ExchangePurseDeposits = { + deposits: depositSigsResp.deposits, + }; + + if (logger.shouldLogTrace()) { + logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); } - default: { - const errResp = await readTalerErrorResponse(httpResp); - return { - type: TaskRunResultType.Error, - errorDetail: errResp, - }; + + const httpResp = await wex.http.fetch(purseDepositUrl.href, { + method: "POST", + body: depositPayload, + cancellationToken: wex.cancellationToken, + }); + + switch (httpResp.status) { + case HttpStatusCode.Ok: { + const resp = await readSuccessResponseJsonOrThrow( + httpResp, + codecForAny(), + ); + logger.trace(`purse deposit response: ${j2s(resp)}`); + continue; + } + case HttpStatusCode.Gone: { + await ctx.abortTransaction(); + return TaskRunResult.backoff(); + } + case HttpStatusCode.Conflict: { + return handlePurseCreationConflict(ctx, peerPullInc, httpResp); + } + default: { + const errResp = await readTalerErrorResponse(httpResp); + return { + type: TaskRunResultType.Error, + errorDetail: errResp, + }; + } } } + + // All batches succeeded, we can transition! + + await ctx.transition(async (r) => { + if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) { + return TransitionResultType.Stay; + } + r.status = PeerPullDebitRecordStatus.Done; + return TransitionResultType.Transition; + }); + return TaskRunResult.finished(); } async function processPeerPullDebitAbortingRefresh( diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts index b6771be89..63a02d7a7 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -20,6 +20,7 @@ import { CheckPeerPushDebitResponse, CoinRefreshRequest, ContractTermsUtil, + ExchangePurseDeposits, HttpStatusCode, InitiatePeerPushDebitRequest, InitiatePeerPushDebitResponse, @@ -564,12 +565,6 @@ async function processPeerPushDebitCreateReserve( peerPushInitiation.coinSel, ); - const depositSigsResp = await wex.cryptoApi.signPurseDeposits({ - exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, - pursePub: peerPushInitiation.pursePub, - coins, - }); - const encryptContractRequest: EncryptContractRequest = { contractTerms: contractTermsRecord.contractTermsRaw, mergePriv: peerPushInitiation.mergePriv, @@ -580,66 +575,115 @@ async function processPeerPushDebitCreateReserve( nonce: peerPushInitiation.contractEncNonce, }; - logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`); - const econtractResp = await wex.cryptoApi.encryptContractForMerge( encryptContractRequest, ); - const createPurseUrl = new URL( - `purses/${peerPushInitiation.pursePub}/create`, - peerPushInitiation.exchangeBaseUrl, - ); + const maxBatchSize = 100; - const reqBody = { - amount: peerPushInitiation.amount, - merge_pub: peerPushInitiation.mergePub, - purse_sig: purseSigResp.sig, - h_contract_terms: hContractTerms, - purse_expiration: timestampProtocolFromDb(purseExpiration), - deposits: depositSigsResp.deposits, - min_age: 0, - econtract: econtractResp.econtract, - }; + for (let i = 0; i < coins.length; i += maxBatchSize) { + const batchSize = Math.min(maxBatchSize, coins.length - i); + const batchCoins = coins.slice(i, i + batchSize); - logger.trace(`request body: ${j2s(reqBody)}`); + const depositSigsResp = await wex.cryptoApi.signPurseDeposits({ + exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, + pursePub: peerPushInitiation.pursePub, + coins: batchCoins, + }); - const httpResp = await wex.http.fetch(createPurseUrl.href, { - method: "POST", - body: reqBody, - cancellationToken: wex.cancellationToken, - }); + if (i == 0) { + // First batch creates the purse! - { - const resp = await httpResp.json(); - logger.info(`resp: ${j2s(resp)}`); - } + logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`); - switch (httpResp.status) { - case HttpStatusCode.Ok: - break; - case HttpStatusCode.Forbidden: { - // FIXME: Store this error! - await ctx.failTransaction(); - return TaskRunResult.finished(); - } - case HttpStatusCode.Conflict: { - // Handle double-spending - return handlePurseCreationConflict(wex, peerPushInitiation, httpResp); - } - default: { - const errResp = await readTalerErrorResponse(httpResp); - return { - type: TaskRunResultType.Error, - errorDetail: errResp, + const createPurseUrl = new URL( + `purses/${peerPushInitiation.pursePub}/create`, + peerPushInitiation.exchangeBaseUrl, + ); + + const reqBody = { + amount: peerPushInitiation.amount, + merge_pub: peerPushInitiation.mergePub, + purse_sig: purseSigResp.sig, + h_contract_terms: hContractTerms, + purse_expiration: timestampProtocolFromDb(purseExpiration), + deposits: depositSigsResp.deposits, + min_age: 0, + econtract: econtractResp.econtract, }; + + if (logger.shouldLogTrace()) { + logger.trace(`request body: ${j2s(reqBody)}`); + } + + const httpResp = await wex.http.fetch(createPurseUrl.href, { + method: "POST", + body: reqBody, + cancellationToken: wex.cancellationToken, + }); + + switch (httpResp.status) { + case HttpStatusCode.Ok: + // Possibly on to the next batch. + continue; + case HttpStatusCode.Forbidden: { + // FIXME: Store this error! + await ctx.failTransaction(); + return TaskRunResult.finished(); + } + case HttpStatusCode.Conflict: { + // Handle double-spending + return handlePurseCreationConflict(wex, peerPushInitiation, httpResp); + } + default: { + const errResp = await readTalerErrorResponse(httpResp); + return { + type: TaskRunResultType.Error, + errorDetail: errResp, + }; + } + } + } else { + const purseDepositUrl = new URL( + `purses/${pursePub}/deposit`, + peerPushInitiation.exchangeBaseUrl, + ); + + const depositPayload: ExchangePurseDeposits = { + deposits: depositSigsResp.deposits, + }; + + const httpResp = await wex.http.fetch(purseDepositUrl.href, { + method: "POST", + body: depositPayload, + cancellationToken: wex.cancellationToken, + }); + + switch (httpResp.status) { + case HttpStatusCode.Ok: + // Possibly on to the next batch. + continue; + case HttpStatusCode.Forbidden: { + // FIXME: Store this error! + await ctx.failTransaction(); + return TaskRunResult.finished(); + } + case HttpStatusCode.Conflict: { + // Handle double-spending + return handlePurseCreationConflict(wex, peerPushInitiation, httpResp); + } + default: { + const errResp = await readTalerErrorResponse(httpResp); + return { + type: TaskRunResultType.Error, + errorDetail: errResp, + }; + } + } } } - if (httpResp.status !== HttpStatusCode.Ok) { - // FIXME: do proper error reporting - throw Error("got error response from exchange"); - } + // All batches done! await transitionPeerPushDebitTransaction(wex, pursePub, { stFrom: PeerPushDebitStatus.PendingCreatePurse, |