diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer.ts | 283 |
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( ), }; } - - |