diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/deposits.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 470 |
1 files changed, 0 insertions, 470 deletions
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts deleted file mode 100644 index 740242050..000000000 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ /dev/null @@ -1,470 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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/> - */ - -import { - Amounts, - buildCodecForObject, - canonicalJson, - Codec, - codecForString, - codecForTimestamp, - codecOptional, - ContractTerms, - CreateDepositGroupRequest, - CreateDepositGroupResponse, - durationFromSpec, - getTimestampNow, - Logger, - NotificationType, - parsePaytoUri, - TalerErrorDetails, - Timestamp, - timestampAddDuration, - timestampTruncateToSecond, - TrackDepositGroupRequest, - TrackDepositGroupResponse, - URL, -} from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../common.js"; -import { kdf } from "@gnu-taler/taler-util"; -import { - encodeCrock, - getRandomBytes, - stringToBytes, -} from "@gnu-taler/taler-util"; -import { DepositGroupRecord } from "../db.js"; -import { guardOperationException } from "../errors.js"; -import { selectPayCoins } from "../util/coinSelection.js"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; -import { getExchangeDetails } from "./exchanges.js"; -import { - applyCoinSpend, - extractContractData, - generateDepositPermissions, - getCandidatePayCoins, - getEffectiveDepositAmount, - getTotalPaymentCost, -} from "./pay.js"; - -/** - * Logger. - */ -const logger = new Logger("deposits.ts"); - -interface DepositSuccess { - // Optional base URL of the exchange for looking up wire transfers - // associated with this transaction. If not given, - // the base URL is the same as the one used for this request. - // Can be used if the base URL for /transactions/ differs from that - // for /coins/, i.e. for load balancing. Clients SHOULD - // respect the transaction_base_url if provided. Any HTTP server - // belonging to an exchange MUST generate a 307 or 308 redirection - // to the correct base URL should a client uses the wrong base - // URL, or if the base URL has changed since the deposit. - transaction_base_url?: string; - - // timestamp when the deposit was received by the exchange. - exchange_timestamp: Timestamp; - - // the EdDSA signature of TALER_DepositConfirmationPS using a current - // signing key of the exchange affirming the successful - // deposit and that the exchange will transfer the funds after the refund - // deadline, or as soon as possible if the refund deadline is zero. - exchange_sig: string; - - // public EdDSA key of the exchange that was used to - // generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: string; -} - -const codecForDepositSuccess = (): Codec<DepositSuccess> => - buildCodecForObject<DepositSuccess>() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) - .property("exchange_timestamp", codecForTimestamp) - .property("transaction_base_url", codecOptional(codecForString())) - .build("DepositSuccess"); - -function hashWire(paytoUri: string, salt: string): string { - const r = kdf( - 64, - stringToBytes(paytoUri + "\0"), - stringToBytes(salt + "\0"), - stringToBytes("merchant-wire-signature"), - ); - return encodeCrock(r); -} - -async function resetDepositGroupRetry( - ws: InternalWalletState, - depositGroupId: string, -): Promise<void> { - await ws.db - .mktx((x) => ({ - depositGroups: x.depositGroups, - })) - .runReadWrite(async (tx) => { - const x = await tx.depositGroups.get(depositGroupId); - if (x) { - x.retryInfo = initRetryInfo(); - await tx.depositGroups.put(x); - } - }); -} - -async function incrementDepositRetry( - ws: InternalWalletState, - depositGroupId: string, - err: TalerErrorDetails | undefined, -): Promise<void> { - await ws.db - .mktx((x) => ({ depositGroups: x.depositGroups })) - .runReadWrite(async (tx) => { - const r = await tx.depositGroups.get(depositGroupId); - if (!r) { - return; - } - if (!r.retryInfo) { - return; - } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); - r.lastError = err; - await tx.depositGroups.put(r); - }); - if (err) { - ws.notify({ type: NotificationType.DepositOperationError, error: err }); - } -} - -export async function processDepositGroup( - ws: InternalWalletState, - depositGroupId: string, - forceNow = false, -): Promise<void> { - await ws.memoProcessDeposit.memo(depositGroupId, async () => { - const onOpErr = (e: TalerErrorDetails): Promise<void> => - incrementDepositRetry(ws, depositGroupId, e); - return await guardOperationException( - async () => await processDepositGroupImpl(ws, depositGroupId, forceNow), - onOpErr, - ); - }); -} - -async function processDepositGroupImpl( - ws: InternalWalletState, - depositGroupId: string, - forceNow: boolean = false, -): Promise<void> { - if (forceNow) { - await resetDepositGroupRetry(ws, depositGroupId); - } - const depositGroup = await ws.db - .mktx((x) => ({ - depositGroups: x.depositGroups, - })) - .runReadOnly(async (tx) => { - return tx.depositGroups.get(depositGroupId); - }); - if (!depositGroup) { - logger.warn(`deposit group ${depositGroupId} not found`); - return; - } - if (depositGroup.timestampFinished) { - logger.trace(`deposit group ${depositGroupId} already finished`); - return; - } - - const contractData = extractContractData( - depositGroup.contractTermsRaw, - depositGroup.contractTermsHash, - "", - ); - - const depositPermissions = await generateDepositPermissions( - ws, - depositGroup.payCoinSelection, - contractData, - ); - - for (let i = 0; i < depositPermissions.length; i++) { - if (depositGroup.depositedPerCoin[i]) { - continue; - } - const perm = depositPermissions[i]; - const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); - const httpResp = await ws.http.postJson(url.href, { - contribution: Amounts.stringify(perm.contribution), - wire: depositGroup.wire, - h_wire: depositGroup.contractTermsRaw.h_wire, - h_contract_terms: depositGroup.contractTermsHash, - ub_sig: perm.ub_sig, - timestamp: depositGroup.contractTermsRaw.timestamp, - wire_transfer_deadline: - depositGroup.contractTermsRaw.wire_transfer_deadline, - refund_deadline: depositGroup.contractTermsRaw.refund_deadline, - coin_sig: perm.coin_sig, - denom_pub_hash: perm.h_denom, - merchant_pub: depositGroup.merchantPub, - }); - await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); - await ws.db - .mktx((x) => ({ depositGroups: x.depositGroups })) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - return; - } - dg.depositedPerCoin[i] = true; - await tx.depositGroups.put(dg); - }); - } - - await ws.db - .mktx((x) => ({ - depositGroups: x.depositGroups, - })) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - return; - } - let allDeposited = true; - for (const d of depositGroup.depositedPerCoin) { - if (!d) { - allDeposited = false; - } - } - if (allDeposited) { - dg.timestampFinished = getTimestampNow(); - delete dg.lastError; - delete dg.retryInfo; - await tx.depositGroups.put(dg); - } - }); -} - -export async function trackDepositGroup( - ws: InternalWalletState, - req: TrackDepositGroupRequest, -): Promise<TrackDepositGroupResponse> { - const responses: { - status: number; - body: any; - }[] = []; - const depositGroup = await ws.db - .mktx((x) => ({ - depositGroups: x.depositGroups, - })) - .runReadOnly(async (tx) => { - return tx.depositGroups.get(req.depositGroupId); - }); - if (!depositGroup) { - throw Error("deposit group not found"); - } - const contractData = extractContractData( - depositGroup.contractTermsRaw, - depositGroup.contractTermsHash, - "", - ); - - const depositPermissions = await generateDepositPermissions( - ws, - depositGroup.payCoinSelection, - contractData, - ); - - const wireHash = depositGroup.contractTermsRaw.h_wire; - - for (const dp of depositPermissions) { - const url = new URL( - `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`, - dp.exchange_url, - ); - const sig = await ws.cryptoApi.signTrackTransaction({ - coinPub: dp.coin_pub, - contractTermsHash: depositGroup.contractTermsHash, - merchantPriv: depositGroup.merchantPriv, - merchantPub: depositGroup.merchantPub, - wireHash, - }); - url.searchParams.set("merchant_sig", sig); - const httpResp = await ws.http.get(url.href); - const body = await httpResp.json(); - responses.push({ - body, - status: httpResp.status, - }); - } - return { - responses, - }; -} - -export async function createDepositGroup( - ws: InternalWalletState, - req: CreateDepositGroupRequest, -): Promise<CreateDepositGroupResponse> { - const p = parsePaytoUri(req.depositPaytoUri); - if (!p) { - throw Error("invalid payto URI"); - } - - const amount = Amounts.parseOrThrow(req.amount); - - const exchangeInfos: { url: string; master_pub: string }[] = []; - - await ws.db - .mktx((x) => ({ - exchanges: x.exchanges, - exchangeDetails: x.exchangeDetails, - })) - .runReadOnly(async (tx) => { - const allExchanges = await tx.exchanges.iter().toArray(); - for (const e of allExchanges) { - const details = await getExchangeDetails(tx, e.baseUrl); - if (!details) { - continue; - } - exchangeInfos.push({ - master_pub: details.masterPublicKey, - url: e.baseUrl, - }); - } - }); - - const timestamp = getTimestampNow(); - const timestampRound = timestampTruncateToSecond(timestamp); - const noncePair = await ws.cryptoApi.createEddsaKeypair(); - const merchantPair = await ws.cryptoApi.createEddsaKeypair(); - const wireSalt = encodeCrock(getRandomBytes(64)); - const wireHash = hashWire(req.depositPaytoUri, wireSalt); - const contractTerms: ContractTerms = { - auditors: [], - exchanges: exchangeInfos, - amount: req.amount, - max_fee: Amounts.stringify(amount), - max_wire_fee: Amounts.stringify(amount), - wire_method: p.targetType, - timestamp: timestampRound, - merchant_base_url: "", - summary: "", - nonce: noncePair.pub, - wire_transfer_deadline: timestampRound, - order_id: "", - h_wire: wireHash, - pay_deadline: timestampAddDuration( - timestampRound, - durationFromSpec({ hours: 1 }), - ), - merchant: { - name: "", - }, - merchant_pub: merchantPair.pub, - refund_deadline: { t_ms: 0 }, - }; - - const contractTermsHash = await ws.cryptoApi.hashString( - canonicalJson(contractTerms), - ); - - const contractData = extractContractData( - contractTerms, - contractTermsHash, - "", - ); - - const candidates = await getCandidatePayCoins(ws, { - allowedAuditors: contractData.allowedAuditors, - allowedExchanges: contractData.allowedExchanges, - amount: contractData.amount, - maxDepositFee: contractData.maxDepositFee, - maxWireFee: contractData.maxWireFee, - timestamp: contractData.timestamp, - wireFeeAmortization: contractData.wireFeeAmortization, - wireMethod: contractData.wireMethod, - }); - - const payCoinSel = selectPayCoins({ - candidates, - contractTermsAmount: contractData.amount, - depositFeeLimit: contractData.maxDepositFee, - wireFeeAmortization: contractData.wireFeeAmortization ?? 1, - wireFeeLimit: contractData.maxWireFee, - prevPayCoins: [], - }); - - if (!payCoinSel) { - throw Error("insufficient funds"); - } - - const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel); - - const depositGroupId = encodeCrock(getRandomBytes(32)); - - const effectiveDepositAmount = await getEffectiveDepositAmount( - ws, - p.targetType, - payCoinSel, - ); - - const depositGroup: DepositGroupRecord = { - contractTermsHash, - contractTermsRaw: contractTerms, - depositGroupId, - noncePriv: noncePair.priv, - noncePub: noncePair.pub, - timestampCreated: timestamp, - timestampFinished: undefined, - payCoinSelection: payCoinSel, - payCoinSelectionUid: encodeCrock(getRandomBytes(32)), - depositedPerCoin: payCoinSel.coinPubs.map(() => false), - merchantPriv: merchantPair.priv, - merchantPub: merchantPair.pub, - totalPayCost: totalDepositCost, - effectiveDepositAmount, - wire: { - payto_uri: req.depositPaytoUri, - salt: wireSalt, - }, - retryInfo: initRetryInfo(), - lastError: undefined, - }; - - await ws.db - .mktx((x) => ({ - depositGroups: x.depositGroups, - coins: x.coins, - refreshGroups: x.refreshGroups, - denominations: x.denominations, - })) - .runReadWrite(async (tx) => { - await applyCoinSpend( - ws, - tx, - payCoinSel, - `deposit-group:${depositGroup.depositGroupId}`, - ); - await tx.depositGroups.put(depositGroup); - }); - - return { depositGroupId }; -} |