commit 63cd73839548b6c00aa400746d2f84b2b973b098
parent 8fe4b0a52ba9754f8a64bf7951322f542cf37052
Author: Florian Dold <florian@dold.me>
Date: Wed, 23 Apr 2025 21:03:55 +0200
wallet-core: handle conflict in peer-push-credit
Diffstat:
6 files changed, 62 insertions(+), 14 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -76,7 +76,10 @@ function DefaultTestingContext({
url: new URL("/", "http://localhost"),
hints: [],
lib: {
- exchange: undefined!, //FIXME: mock
+ exchange: undefined!, // FIXME: mock
+ },
+ unthrottledApi: {
+ exchange: undefined!, // FIXME: mock
},
onActivity: () => null!,
};
diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx
@@ -64,7 +64,10 @@ function getWrapperForGroup(): FunctionComponent {
url: new URL("/", "http://localhost"),
hints: [],
lib: {
- exchange: undefined!, //FIXME: mock
+ exchange: undefined!, // FIXME: mock
+ },
+ unthrottledApi: {
+ exchange: undefined!, // FIXME: mock
},
onActivity: () => null!,
};
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts
@@ -340,8 +340,7 @@ export async function runPeerPushTest(t: GlobalTestState) {
wallet2.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare2.transactionId,
txState: {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Merge,
+ major: TransactionMajorState.Aborted,
},
logId: `exp-wallet2`,
timeout: { seconds: 10 },
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
@@ -50,6 +50,7 @@ import {
BatchWithdrawResponse,
ExchangeKycUploadFormRequest,
ExchangeLegacyBatchWithdrawRequest,
+ ExchangeMergeConflictResponse,
ExchangeMergeSuccessResponse,
ExchangePurseMergeRequest,
ExchangeVersionResponse,
@@ -64,6 +65,7 @@ import {
codecForEventCounter,
codecForExchangeConfig,
codecForExchangeKeysResponse,
+ codecForExchangeMergeConflictResponse,
codecForExchangeMergeSuccessResponse,
codecForExchangeTransferList,
codecForKycProcessClientInformation,
@@ -483,14 +485,6 @@ export class TalerExchangeHttpClient {
}
/**
- * https://docs.taler.net/core/api-exchange.html#post--refreshes-$RCH-reveal
- *
- */
- async releaveCoin(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
* https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-link
*
*/
@@ -584,7 +578,6 @@ export class TalerExchangeHttpClient {
* POST /purses/$PURSE_PUB/merge
*
* https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- *
*/
async postPurseMerge(args: {
pursePub: string;
@@ -596,6 +589,10 @@ export class TalerExchangeHttpClient {
HttpStatusCode.UnavailableForLegalReasons,
LegitimizationNeededResponse
>
+ | OperationAlternative<
+ HttpStatusCode.Conflict,
+ ExchangeMergeConflictResponse
+ >
> {
const mergePurseUrl = new URL(
`purses/${args.pursePub}/merge`,
@@ -614,8 +611,15 @@ export class TalerExchangeHttpClient {
resp.status,
codecForLegitimizationNeededResponse(),
);
+ case HttpStatusCode.Conflict:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForExchangeMergeConflictResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
- throw Error("not yet implemented");
}
/**
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -1268,6 +1268,35 @@ export const codecForExchangeMergeSuccessResponse =
.build("ExchangeMergeSuccessResponse");
/**
+ * Doc name: api-exchange/MergeConflict
+ */
+export interface ExchangeMergeConflictResponse {
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: Timestamp;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // Base URL of the exchange receiving the payment, only present
+ // if the exchange hosting the reserve is not this exchange.
+ partner_url?: string;
+
+ // Public key of the reserve that the purse was merged into.
+ reserve_pub: EddsaPublicKeyString;
+}
+
+export const codecForExchangeMergeConflictResponse =
+ (): Codec<ExchangeMergeConflictResponse> =>
+ buildCodecForObject<ExchangeMergeConflictResponse>()
+ .property("merge_timestamp", codecForTimestamp)
+ .property("merge_sig", codecForString())
+ .property("reserve_pub", codecForString())
+ .property("partner_url", codecOptional(codecForString()))
+ .build("ExchangeMergeConflictResponse");
+
+/**
* Contract terms between two wallets (as opposed to a merchant and wallet).
*/
export interface PeerContractTerms {
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -920,6 +920,16 @@ async function handlePendingMerge(
logger.info(`kyc legitimization needed response: ${j2s(mergeResp.body)}`);
return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp);
}
+ case HttpStatusCode.Conflict: {
+ // FIXME: Check signature.
+ await ctx.transitionStatus(
+ PeerPushCreditStatus.PendingMerge,
+ PeerPushCreditStatus.Aborted,
+ );
+ return TaskRunResult.progress();
+ }
+ default:
+ assertUnreachable(mergeResp);
}
const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(wex, {