wallet-core: allow failure result in peer payment coin selection

This commit is contained in:
Florian Dold 2023-01-06 11:08:45 +01:00
parent 80639429a2
commit c2c35925bb
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 97 additions and 39 deletions

View File

@ -3240,6 +3240,14 @@ export enum TalerErrorCode {
WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE = 7026,
/**
* The wallet does not have sufficient balance to create a peer push payment.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
/**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).

View File

@ -419,7 +419,10 @@ export const codecForPreparePayResultInsufficientBalance =
"status",
codecForConstString(PreparePayResultType.InsufficientBalance),
)
.property("balanceDetails", codecForPayMerchantInsufficientBalanceDetails())
.property(
"balanceDetails",
codecForPayMerchantInsufficientBalanceDetails(),
)
.build("PreparePayResultInsufficientBalance");
export const codecForPreparePayResultAlreadyConfirmed =
@ -2084,7 +2087,6 @@ export interface InitiatePeerPullPaymentResponse {
transactionId: string;
}
/**
* Detailed reason for why the wallet's balance is insufficient.
*/
@ -2124,23 +2126,58 @@ export interface PayMerchantInsufficientBalanceDetails {
* (i.e. balanceMechantWireable >= amountRequested),
* this field contains an estimate of the amount that would additionally
* be required to cover the fees.
*
*
* It is not possible to give an exact value here, since it depends
* on the coin selection for the amount that would be additionally withdrawn.
*/
feeGapEstimate: AmountString;
}
const codecForPayMerchantInsufficientBalanceDetails =
(): Codec<PayMerchantInsufficientBalanceDetails> =>
buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
.property("amountRequested", codecForAmountString())
.property("balanceAgeAcceptable", codecForAmountString())
.property("balanceAvailable", codecForAmountString())
.property("balanceMaterial", codecForAmountString())
.property("balanceMerchantAcceptable", codecForAmountString())
.property("balanceMerchantDepositable", codecForAmountString())
.property("feeGapEstimate", codecForAmountString())
.build("PayMerchantInsufficientBalanceDetails");
export const codecForPayMerchantInsufficientBalanceDetails =
(): Codec<PayMerchantInsufficientBalanceDetails> =>
buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
.property("amountRequested", codecForAmountString())
.property("balanceAgeAcceptable", codecForAmountString())
.property("balanceAvailable", codecForAmountString())
.property("balanceMaterial", codecForAmountString())
.property("balanceMerchantAcceptable", codecForAmountString())
.property("balanceMerchantDepositable", codecForAmountString())
.property("feeGapEstimate", codecForAmountString())
.build("PayMerchantInsufficientBalanceDetails");
/**
* Detailed reason for why the wallet's balance is insufficient.
*/
export interface PayPeerInsufficientBalanceDetails {
/**
* Amount requested by the merchant.
*/
amountRequested: AmountString;
/**
* Balance of type "available" (see balance.ts for definition).
*/
balanceAvailable: AmountString;
/**
* Balance of type "material" (see balance.ts for definition).
*/
balanceMaterial: AmountString;
/**
* Acceptable balance based on restrictions on which
* exchange can be used.
*/
balanceExchangeAcceptable: AmountString
/**
* If the payment would succeed without fees
* (i.e. balanceExchangeAcceptable >= amountRequested),
* this field contains an estimate of the amount that would additionally
* be required to cover the fees.
*
* It is not possible to give an exact value here, since it depends
* on the coin selection for the amount that would be additionally withdrawn.
*/
feeGapEstimate: AmountString;
}

View File

@ -18,7 +18,6 @@
* Imports.
*/
import {
AbsoluteTime,
AcceptPeerPullPaymentRequest,
AcceptPeerPullPaymentResponse,
AcceptPeerPushPaymentRequest,
@ -41,7 +40,6 @@ import {
constructPayPushUri,
ContractTermsUtil,
decodeCrock,
Duration,
eddsaGetPublic,
encodeCrock,
ExchangePurseDeposits,
@ -56,6 +54,7 @@ import {
Logger,
parsePayPullUri,
parsePayPushUri,
PayPeerInsufficientBalanceDetails,
PeerContractTerms,
PreparePeerPullPaymentRequest,
PreparePeerPullPaymentResponse,
@ -132,6 +131,12 @@ interface CoinInfo {
ageCommitmentProof?: AgeCommitmentProof;
}
export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelection }
| {
type: "failure";
};
export async function selectPeerCoins(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
@ -140,7 +145,7 @@ export async function selectPeerCoins(
coins: typeof WalletStoresV1.coins;
}>,
instructedAmount: AmountJson,
): Promise<PeerCoinSelection | undefined> {
): Promise<SelectPeerCoinsResult> {
const exchanges = await tx.exchanges.iter().toArray();
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== instructedAmount.currency) {
@ -218,11 +223,11 @@ export async function selectPeerCoins(
coins: resCoins,
depositFees: depositFeesAcc,
};
return res;
return { type: "success", result: res };
}
continue;
}
return undefined;
return { type: "failure" };
}
export async function preparePeerPushPayment(
@ -258,7 +263,7 @@ export async function initiatePeerToPeerPush(
pursePub: pursePair.pub,
});
const coinSelRes: PeerCoinSelection | undefined = await ws.db
const coinSelRes: SelectPeerCoinsResult = await ws.db
.mktx((x) => [
x.exchanges,
x.contractTerms,
@ -270,11 +275,13 @@ export async function initiatePeerToPeerPush(
x.peerPushPaymentInitiations,
])
.runReadWrite(async (tx) => {
const sel = await selectPeerCoins(ws, tx, instructedAmount);
if (!sel) {
return undefined;
const selRes = await selectPeerCoins(ws, tx, instructedAmount);
if (selRes.type === "failure") {
return selRes;
}
const sel = selRes.result;
await spendCoins(ws, tx, {
allocationId: `txn:peer-push-debit:${pursePair.pub}`,
coinPubs: sel.coins.map((x) => x.coinPub),
@ -304,11 +311,12 @@ export async function initiatePeerToPeerPush(
contractTermsRaw: contractTerms,
});
return sel;
return selRes;
});
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
if (!coinSelRes) {
if (coinSelRes.type !== "success") {
// FIXME: use error code with details here
throw Error("insufficient balance");
}
@ -322,14 +330,14 @@ export async function initiatePeerToPeerPush(
});
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
pursePub: pursePair.pub,
coins: coinSelRes.coins,
coins: coinSelRes.result.coins,
});
const createPurseUrl = new URL(
`purses/${pursePair.pub}/create`,
coinSelRes.exchangeBaseUrl,
coinSelRes.result.exchangeBaseUrl,
);
const httpResp = await ws.http.postJson(createPurseUrl.href, {
@ -355,9 +363,9 @@ export async function initiatePeerToPeerPush(
contractPriv: econtractResp.contractPriv,
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
talerUri: constructPayPushUri({
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
contractPriv: econtractResp.contractPriv,
}),
transactionId: makeTransactionId(
@ -627,7 +635,7 @@ export async function acceptPeerPullPayment(
const instructedAmount = Amounts.parseOrThrow(
peerPullInc.contractTerms.amount,
);
const coinSelRes: PeerCoinSelection | undefined = await ws.db
const coinSelRes: SelectPeerCoinsResult = await ws.db
.mktx((x) => [
x.exchanges,
x.coins,
@ -637,11 +645,13 @@ export async function acceptPeerPullPayment(
x.coinAvailability,
])
.runReadWrite(async (tx) => {
const sel = await selectPeerCoins(ws, tx, instructedAmount);
if (!sel) {
return undefined;
const selRes = await selectPeerCoins(ws, tx, instructedAmount);
if (selRes.type !== "success") {
return selRes;
}
const sel = selRes.result;
await spendCoins(ws, tx, {
allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`,
coinPubs: sel.coins.map((x) => x.coinPub),
@ -660,25 +670,27 @@ export async function acceptPeerPullPayment(
pi.status = PeerPullPaymentIncomingStatus.Accepted;
await tx.peerPullPaymentIncoming.put(pi);
return sel;
return selRes;
});
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
if (!coinSelRes) {
if (coinSelRes.type !== "success") {
throw Error("insufficient balance");
}
const pursePub = peerPullInc.pursePub;
const coinSel = coinSelRes.result;
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
exchangeBaseUrl: coinSel.exchangeBaseUrl,
pursePub,
coins: coinSelRes.coins,
coins: coinSel.coins,
});
const purseDepositUrl = new URL(
`purses/${pursePub}/deposit`,
coinSelRes.exchangeBaseUrl,
coinSel.exchangeBaseUrl,
);
const depositPayload: ExchangePurseDeposits = {
@ -770,6 +782,7 @@ export async function preparePeerPullPayment(
amountRaw: req.amount,
};
}
/**
* Initiate a peer pull payment.
*/