aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts283
1 files changed, 243 insertions, 40 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts
index c1cacead9..eda107bea 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer.ts
@@ -18,6 +18,7 @@
* Imports.
*/
import {
+ AbsoluteTime,
AcceptPeerPullPaymentRequest,
AcceptPeerPullPaymentResponse,
AcceptPeerPushPaymentRequest,
@@ -35,6 +36,7 @@ import {
codecForAmountString,
codecForAny,
codecForExchangeGetContractResponse,
+ codecForPeerContractTerms,
CoinStatus,
constructPayPullUri,
constructPayPushUri,
@@ -545,6 +547,9 @@ export async function initiatePeerPushPayment(
x.peerPushPaymentInitiations,
])
.runReadWrite(async (tx) => {
+ // FIXME: Instead of directly doing a spendCoin here,
+ // we might want to mark the coins as used and spend them
+ // after we've been able to create the purse.
await spendCoins(ws, tx, {
allocationId: `txn:peer-push-debit:${pursePair.pub}`,
coinPubs: sel.coins.map((x) => x.coinPub),
@@ -846,7 +851,77 @@ export async function acceptPeerPushPayment(
};
}
-export async function acceptPeerPullPayment(
+export async function processPeerPullDebit(
+ ws: InternalWalletState,
+ peerPullPaymentIncomingId: string,
+): Promise<OperationAttemptResult> {
+ const peerPullInc = await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadOnly(async (tx) => {
+ return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId);
+ });
+ if (!peerPullInc) {
+ throw Error("peer pull debit not found");
+ }
+ if (peerPullInc.status === PeerPullPaymentIncomingStatus.Accepted) {
+ const pursePub = peerPullInc.pursePub;
+
+ const coinSel = peerPullInc.coinSel;
+ if (!coinSel) {
+ throw Error("invalid state, no coins selected");
+ }
+
+ const coins = await queryCoinInfosForSelection(ws, coinSel);
+
+ const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
+ pursePub: peerPullInc.pursePub,
+ coins,
+ });
+
+ const purseDepositUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ peerPullInc.exchangeBaseUrl,
+ );
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
+ }
+
+ const httpResp = await ws.http.postJson(
+ purseDepositUrl.href,
+ depositPayload,
+ );
+ const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
+ logger.trace(`purse deposit response: ${j2s(resp)}`);
+ }
+
+ await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadWrite(async (tx) => {
+ const pi = await tx.peerPullPaymentIncoming.get(
+ peerPullPaymentIncomingId,
+ );
+ if (!pi) {
+ throw Error("peer pull payment not found anymore");
+ }
+ if (pi.status === PeerPullPaymentIncomingStatus.Accepted) {
+ pi.status = PeerPullPaymentIncomingStatus.Paid;
+ }
+ await tx.peerPullPaymentIncoming.put(pi);
+ });
+
+ return {
+ type: OperationAttemptResultType.Finished,
+ result: undefined,
+ };
+}
+
+export async function acceptIncomingPeerPullPayment(
ws: InternalWalletState,
req: AcceptPeerPullPaymentRequest,
): Promise<AcceptPeerPullPaymentResponse> {
@@ -885,7 +960,7 @@ export async function acceptPeerPullPayment(
coinSelRes.result.coins,
);
- await ws.db
+ const ppi = await ws.db
.mktx((x) => [
x.exchanges,
x.coins,
@@ -910,34 +985,26 @@ export async function acceptPeerPullPayment(
if (!pi) {
throw Error();
}
- pi.status = PeerPullPaymentIncomingStatus.Accepted;
- pi.totalCost = Amounts.stringify(totalAmount);
+ if (pi.status === PeerPullPaymentIncomingStatus.Proposed) {
+ pi.status = PeerPullPaymentIncomingStatus.Accepted;
+ pi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ }
await tx.peerPullPaymentIncoming.put(pi);
+ return pi;
});
- const pursePub = peerPullInc.pursePub;
-
- const coinSel = coinSelRes.result;
-
- const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
- exchangeBaseUrl: coinSel.exchangeBaseUrl,
- pursePub,
- coins: coinSel.coins,
- });
-
- const purseDepositUrl = new URL(
- `purses/${pursePub}/deposit`,
- coinSel.exchangeBaseUrl,
+ await runOperationWithErrorReporting(
+ ws,
+ RetryTags.forPeerPullPaymentDebit(ppi),
+ async () => {
+ return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId);
+ },
);
- const depositPayload: ExchangePurseDeposits = {
- deposits: depositSigsResp.deposits,
- };
-
- const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
- const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
- logger.trace(`purse deposit response: ${j2s(resp)}`);
-
return {
transactionId: makeTransactionId(
TransactionType.PeerPullDebit,
@@ -946,14 +1013,38 @@ export async function acceptPeerPullPayment(
};
}
-export async function checkPeerPullPayment(
+/**
+ * Look up information about an incoming peer pull payment.
+ * Store the results in the wallet DB.
+ */
+export async function prepareIncomingPeerPullPayment(
ws: InternalWalletState,
req: CheckPeerPullPaymentRequest,
): Promise<CheckPeerPullPaymentResponse> {
const uri = parsePayPullUri(req.talerUri);
if (!uri) {
- throw Error("got invalid taler://pay-push URI");
+ throw Error("got invalid taler://pay-pull URI");
+ }
+
+ const existingPullIncomingRecord = await ws.db
+ .mktx((x) => [x.peerPullPaymentIncoming])
+ .runReadOnly(async (tx) => {
+ return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([
+ uri.exchangeBaseUrl,
+ uri.contractPriv,
+ ]);
+ });
+
+ if (existingPullIncomingRecord) {
+ return {
+ amount: existingPullIncomingRecord.contractTerms.amount,
+ amountRaw: existingPullIncomingRecord.contractTerms.amount,
+ amountEffective: existingPullIncomingRecord.totalCostEstimated,
+ contractTerms: existingPullIncomingRecord.contractTerms,
+ peerPullPaymentIncomingId:
+ existingPullIncomingRecord.peerPullPaymentIncomingId,
+ };
}
const exchangeBaseUrl = uri.exchangeBaseUrl;
@@ -988,6 +1079,38 @@ export async function checkPeerPullPayment(
const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32));
+ let contractTerms: PeerContractTerms;
+
+ if (dec.contractTerms) {
+ contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
+ // FIXME: Check that the purseStatus balance matches contract terms amount
+ } else {
+ // FIXME: In this case, where do we get the purse expiration from?!
+ // https://bugs.gnunet.org/view.php?id=7706
+ throw Error("pull payments without contract terms not supported yet");
+ }
+
+ // FIXME: Why don't we compute the totalCost here?!
+
+ const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
+
+ const coinSelRes = await selectPeerCoins(ws, instructedAmount);
+ logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+
+ if (coinSelRes.type !== "success") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ }
+
+ const totalAmount = await getTotalPeerPaymentCost(
+ ws,
+ coinSelRes.result.coins,
+ );
+
await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadWrite(async (tx) => {
@@ -997,15 +1120,17 @@ export async function checkPeerPullPayment(
exchangeBaseUrl: exchangeBaseUrl,
pursePub: pursePub,
timestampCreated: TalerProtocolTimestamp.now(),
- contractTerms: dec.contractTerms,
+ contractTerms,
status: PeerPullPaymentIncomingStatus.Proposed,
- totalCost: undefined,
+ totalCostEstimated: Amounts.stringify(totalAmount),
});
});
return {
- amount: purseStatus.balance,
- contractTerms: dec.contractTerms,
+ amount: contractTerms.amount,
+ amountEffective: Amounts.stringify(totalAmount),
+ amountRaw: contractTerms.amount,
+ contractTerms: contractTerms,
peerPullPaymentIncomingId,
};
}
@@ -1119,12 +1244,75 @@ export async function processPeerPullInitiation(
};
}
-export async function preparePeerPullPayment(
+/**
+ * Find a prefered exchange based on when we withdrew last from this exchange.
+ */
+async function getPreferredExchangeForCurrency(
+ ws: InternalWalletState,
+ currency: string,
+): Promise<string | undefined> {
+ // Find an exchange with the matching currency.
+ // Prefer exchanges with the most recent withdrawal.
+ const url = await ws.db
+ .mktx((x) => [x.exchanges])
+ .runReadOnly(async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ let candidate = undefined;
+ for (const e of exchanges) {
+ if (e.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ if (!candidate) {
+ candidate = e;
+ continue;
+ }
+ if (candidate.lastWithdrawal && !e.lastWithdrawal) {
+ continue;
+ }
+ if (candidate.lastWithdrawal && e.lastWithdrawal) {
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromTimestamp(e.lastWithdrawal),
+ AbsoluteTime.fromTimestamp(candidate.lastWithdrawal),
+ ) > 0
+ ) {
+ candidate = e;
+ }
+ }
+ }
+ if (candidate) {
+ return candidate.baseUrl;
+ }
+ return undefined;
+ });
+ return url;
+}
+
+/**
+ * Check fees and available exchanges for a peer push payment initiation.
+ */
+export async function checkPeerPullPaymentInitiation(
ws: InternalWalletState,
req: PreparePeerPullPaymentRequest,
): Promise<PreparePeerPullPaymentResponse> {
- //FIXME: look up for exchange details and use purse fee
+ // FIXME: We don't support exchanges with purse fees yet.
+ // Select an exchange where we have money in the specified currency
+ // FIXME: How do we handle regional currency scopes here? Is it an additional input?
+
+ const currency = Amounts.currencyOf(req.amount);
+ let exchangeUrl;
+ if (req.exchangeBaseUrl) {
+ exchangeUrl = req.exchangeBaseUrl;
+ } else {
+ exchangeUrl = await getPreferredExchangeForCurrency(ws, currency);
+ }
+
+ if (!exchangeUrl) {
+ throw Error("no exchange found for initiating a peer pull payment");
+ }
+
return {
+ exchangeBaseUrl: exchangeUrl,
amountEffective: req.amount,
amountRaw: req.amount,
};
@@ -1137,10 +1325,24 @@ export async function initiatePeerPullPayment(
ws: InternalWalletState,
req: InitiatePeerPullPaymentRequest,
): Promise<InitiatePeerPullPaymentResponse> {
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
+ const currency = Amounts.currencyOf(req.partialContractTerms.amount);
+ let maybeExchangeBaseUrl: string | undefined;
+ if (req.exchangeBaseUrl) {
+ maybeExchangeBaseUrl = req.exchangeBaseUrl;
+ } else {
+ maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency);
+ }
+
+ if (!maybeExchangeBaseUrl) {
+ throw Error("no exchange found for initiating a peer pull payment");
+ }
+
+ const exchangeBaseUrl = maybeExchangeBaseUrl;
+
+ await updateExchangeFromUrl(ws, exchangeBaseUrl);
const mergeReserveInfo = await getMergeReserveInfo(ws, {
- exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
});
const mergeTimestamp = TalerProtocolTimestamp.now();
@@ -1166,7 +1368,7 @@ export async function initiatePeerPullPayment(
await tx.peerPullPaymentInitiations.put({
amount: req.partialContractTerms.amount,
contractTermsHash: hContractTerms,
- exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
mergePriv: mergePair.priv,
@@ -1196,6 +1398,9 @@ export async function initiatePeerPullPayment(
},
);
+ // FIXME: Why do we create this only here?
+ // What if the previous operation didn't succeed?
+
const wg = await internalCreateWithdrawalGroup(ws, {
amount: instructedAmount,
wgInfo: {
@@ -1203,7 +1408,7 @@ export async function initiatePeerPullPayment(
contractTerms,
contractPriv: contractKeyPair.priv,
},
- exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
reserveKeyPair: {
priv: mergeReserveInfo.reservePriv,
@@ -1213,7 +1418,7 @@ export async function initiatePeerPullPayment(
return {
talerUri: constructPayPullUri({
- exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
contractPriv: contractKeyPair.priv,
}),
transactionId: makeTransactionId(
@@ -1222,5 +1427,3 @@ export async function initiatePeerPullPayment(
),
};
}
-
-