wallet-core/packages/taler-wallet-core/src/operations/deposits.ts

954 lines
28 KiB
TypeScript
Raw Normal View History

2021-01-18 23:35:41 +01:00
/*
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/>
*/
/**
* Imports.
*/
2021-01-18 23:35:41 +01:00
import {
2022-03-18 15:32:41 +01:00
AbsoluteTime,
2021-12-23 19:17:36 +01:00
AmountJson,
2021-03-17 17:56:37 +01:00
Amounts,
2022-03-28 23:59:16 +02:00
CancellationToken,
canonicalJson,
codecForDepositSuccess,
2023-01-15 21:48:41 +01:00
codecForTackTransactionAccepted,
codecForTackTransactionWired,
CoinDepositPermission,
2021-03-17 17:56:37 +01:00
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DepositGroupFees,
durationFromSpec,
encodeCrock,
ExchangeDepositRequest,
getRandomBytes,
hashTruncate32,
hashWire,
2023-01-15 21:48:41 +01:00
HttpStatusCode,
j2s,
Logger,
2023-01-15 21:48:41 +01:00
MerchantContractTerms,
2023-04-22 14:17:49 +02:00
NotificationType,
2021-03-17 17:56:37 +01:00
parsePaytoUri,
PayCoinSelection,
2022-05-03 05:16:03 +02:00
PrepareDepositRequest,
PrepareDepositResponse,
RefreshReason,
stringToBytes,
2023-01-15 21:48:41 +01:00
TalerErrorCode,
2022-03-18 15:32:41 +01:00
TalerProtocolTimestamp,
2023-01-15 21:48:41 +01:00
TrackTransaction,
2023-04-22 14:17:49 +02:00
TransactionMajorState,
TransactionMinorState,
2023-04-05 17:38:34 +02:00
TransactionState,
TransactionType,
URL,
WireFee,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
2022-09-05 18:12:30 +02:00
import {
2023-04-06 12:47:34 +02:00
DenominationRecord,
2022-09-05 18:12:30 +02:00
DepositGroupRecord,
OperationStatus,
2023-04-22 14:17:49 +02:00
DepositElementStatus,
2022-09-05 18:12:30 +02:00
} from "../db.js";
2023-02-15 23:32:42 +01:00
import { TalerError } from "@gnu-taler/taler-util";
2023-04-06 12:47:34 +02:00
import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
2023-02-15 23:32:42 +01:00
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
2023-04-22 14:17:49 +02:00
import {
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
import { spendCoins } from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
2021-01-18 23:35:41 +01:00
import {
extractContractData,
generateDepositPermissions,
getTotalPaymentCost,
} from "./pay-merchant.js";
import { selectPayCoinsNew } from "../util/coinSelection.js";
import {
constructTransactionIdentifier,
parseTransactionIdentifier,
} from "./transactions.js";
2021-01-18 23:35:41 +01:00
/**
* Logger.
*/
const logger = new Logger("deposits.ts");
/**
* Get the (DD37-style) transaction status based on the
* database record of a deposit group.
*/
2023-04-22 14:17:49 +02:00
export function computeDepositTransactionStatus(
2023-04-05 17:38:34 +02:00
dg: DepositGroupRecord,
2023-04-22 14:17:49 +02:00
): TransactionState {
2023-04-05 17:38:34 +02:00
switch (dg.operationStatus) {
case OperationStatus.Finished: {
return {
2023-04-22 14:17:49 +02:00
major: TransactionMajorState.Done,
2023-04-05 17:38:34 +02:00
};
}
case OperationStatus.Pending: {
const numTotal = dg.payCoinSelection.coinPubs.length;
let numDeposited = 0;
let numKycRequired = 0;
let numWired = 0;
for (let i = 0; i < numTotal; i++) {
if (dg.depositedPerCoin[i]) {
numDeposited++;
}
switch (dg.transactionPerCoin[i]) {
2023-04-22 14:17:49 +02:00
case DepositElementStatus.KycRequired:
2023-04-05 17:38:34 +02:00
numKycRequired++;
break;
2023-04-22 14:17:49 +02:00
case DepositElementStatus.Wired:
2023-04-05 17:38:34 +02:00
numWired++;
break;
}
}
if (numKycRequired > 0) {
return {
2023-04-22 14:17:49 +02:00
major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycRequired,
2023-04-05 17:38:34 +02:00
};
}
if (numDeposited == numTotal) {
return {
2023-04-22 14:17:49 +02:00
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Track,
2023-04-05 17:38:34 +02:00
};
}
return {
2023-04-22 14:17:49 +02:00
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Deposit,
2023-04-05 17:38:34 +02:00
};
}
default:
throw Error("unexpected deposit group state");
}
}
export async function suspendDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
throw Error("not implemented");
}
export async function abortDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
throw Error("not implemented");
}
/**
* Check KYC status with the exchange, throw an appropriate exception when KYC
* is required.
2023-04-05 17:38:34 +02:00
*
* FIXME: Why does this throw an exception when KYC is required?
* Should we not return some proper result record here?
*/
async function checkDepositKycStatus(
ws: InternalWalletState,
exchangeUrl: string,
kycInfo: KycPendingInfo,
userType: KycUserType,
): Promise<void> {
const url = new URL(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
exchangeUrl,
);
logger.info(`kyc url ${url.href}`);
const kycStatusReq = await ws.http.fetch(url.href, {
method: "GET",
});
if (kycStatusReq.status === HttpStatusCode.Ok) {
logger.warn("kyc requested, but already fulfilled");
return;
} else if (kycStatusReq.status === HttpStatusCode.Accepted) {
const kycStatus = await kycStatusReq.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
// FIXME: This error code is totally wrong
throw TalerError.fromDetail(
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
{
kycUrl: kycStatus.kyc_url,
},
`KYC check required for deposit`,
);
} else {
throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
}
}
2022-03-28 23:59:16 +02:00
/**
2023-04-05 17:38:34 +02:00
* Process a deposit group that is not in its final state yet.
2022-03-28 23:59:16 +02:00
*/
2022-09-05 18:12:30 +02:00
export async function processDepositGroup(
2021-01-18 23:35:41 +01:00
ws: InternalWalletState,
depositGroupId: string,
2022-03-28 23:59:16 +02:00
options: {
cancellationToken?: CancellationToken;
} = {},
2022-09-05 18:12:30 +02:00
): Promise<OperationAttemptResult> {
2021-06-09 15:14:17 +02:00
const depositGroup = await ws.db
.mktx((x) => [x.depositGroups])
2021-06-09 15:14:17 +02:00
.runReadOnly(async (tx) => {
return tx.depositGroups.get(depositGroupId);
});
2021-01-18 23:35:41 +01:00
if (!depositGroup) {
logger.warn(`deposit group ${depositGroupId} not found`);
2022-09-05 18:12:30 +02:00
return OperationAttemptResult.finishedEmpty();
2021-01-18 23:35:41 +01:00
}
if (depositGroup.timestampFinished) {
logger.trace(`deposit group ${depositGroupId} already finished`);
2022-09-05 18:12:30 +02:00
return OperationAttemptResult.finishedEmpty();
2021-01-18 23:35:41 +01:00
}
2023-04-22 14:17:49 +02:00
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
const txStateOld = computeDepositTransactionStatus(depositGroup);
2021-01-18 23:35:41 +01:00
const contractData = extractContractData(
depositGroup.contractTermsRaw,
depositGroup.contractTermsHash,
"",
);
2022-03-28 23:59:16 +02:00
// Check for cancellation before expensive operations.
options.cancellationToken?.throwIfCancelled();
// FIXME: Cache these!
2021-01-18 23:35:41 +01:00
const depositPermissions = await generateDepositPermissions(
ws,
depositGroup.payCoinSelection,
contractData,
);
for (let i = 0; i < depositPermissions.length; i++) {
const perm = depositPermissions[i];
2023-01-15 21:48:41 +01:00
2023-04-22 14:17:49 +02:00
let updatedDeposit: boolean = false;
2023-01-15 21:48:41 +01:00
if (!depositGroup.depositedPerCoin[i]) {
const requestBody: ExchangeDepositRequest = {
contribution: Amounts.stringify(perm.contribution),
merchant_payto_uri: depositGroup.wire.payto_uri,
wire_salt: depositGroup.wire.salt,
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,
h_age_commitment: perm.h_age_commitment,
};
// Check for cancellation before making network request.
options.cancellationToken?.throwIfCancelled();
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
logger.info(`depositing to ${url}`);
2023-04-05 17:38:34 +02:00
const httpResp = await ws.http.fetch(url.href, {
method: "POST",
body: requestBody,
2023-01-15 21:48:41 +01:00
cancellationToken: options.cancellationToken,
2021-06-09 15:14:17 +02:00
});
2023-01-15 21:48:41 +01:00
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
updatedDeposit = true;
}
2023-04-22 14:17:49 +02:00
let updatedTxStatus: DepositElementStatus | undefined = undefined;
type ValueOf<T> = T[keyof T];
let newWiredTransaction:
| {
id: string;
value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
}
| undefined;
2023-04-22 14:17:49 +02:00
if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(ws, depositGroup, perm);
if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) {
2023-04-22 14:17:49 +02:00
updatedTxStatus = DepositElementStatus.KycRequired;
const { requirement_row: requirementRow } = track;
const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
await checkDepositKycStatus(
ws,
perm.exchange_url,
{ paytoHash, requirementRow },
"individual",
);
} else {
2023-04-22 14:17:49 +02:00
updatedTxStatus = DepositElementStatus.Accepted;
}
} else if (track.type === "wired") {
2023-04-22 14:17:49 +02:00
updatedTxStatus = DepositElementStatus.Wired;
const payto = parsePaytoUri(depositGroup.wire.payto_uri);
if (!payto) {
throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
}
const fee = await getExchangeWireFee(
ws,
payto.targetType,
perm.exchange_url,
track.execution_time,
);
const raw = Amounts.parseOrThrow(track.coin_contribution);
const wireFee = Amounts.parseOrThrow(fee.wireFee);
newWiredTransaction = {
value: {
amountRaw: Amounts.stringify(raw),
wireFee: Amounts.stringify(wireFee),
exchangePub: track.exchange_pub,
timestampExecuted: track.execution_time,
wireTransferId: track.wtid,
},
id: track.exchange_sig,
};
} else {
2023-04-22 14:17:49 +02:00
updatedTxStatus = DepositElementStatus.Unknown;
}
2023-01-15 21:48:41 +01:00
}
2023-04-22 14:17:49 +02:00
if (updatedTxStatus !== undefined || updatedDeposit) {
2023-01-15 21:48:41 +01:00
await ws.db
.mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return;
}
if (updatedDeposit !== undefined) {
dg.depositedPerCoin[i] = updatedDeposit;
}
if (updatedTxStatus !== undefined) {
dg.transactionPerCoin[i] = updatedTxStatus;
}
if (newWiredTransaction) {
if (!dg.trackingState) {
dg.trackingState = {};
}
dg.trackingState[newWiredTransaction.id] =
newWiredTransaction.value;
}
2023-01-15 21:48:41 +01:00
await tx.depositGroups.put(dg);
});
}
2021-06-09 15:14:17 +02:00
}
2023-04-22 14:17:49 +02:00
const txStatusNew = await ws.db
.mktx((x) => [x.depositGroups])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
2021-01-18 23:35:41 +01:00
if (!dg) {
2023-04-22 14:17:49 +02:00
return undefined;
2021-01-18 23:35:41 +01:00
}
2023-01-15 21:48:41 +01:00
let allDepositedAndWired = true;
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
if (
!depositGroup.depositedPerCoin[i] ||
2023-04-22 14:17:49 +02:00
depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
2023-01-15 21:48:41 +01:00
) {
allDepositedAndWired = false;
break;
2021-06-09 15:14:17 +02:00
}
2021-01-18 23:35:41 +01:00
}
2023-01-15 21:48:41 +01:00
if (allDepositedAndWired) {
2022-03-18 15:32:41 +01:00
dg.timestampFinished = TalerProtocolTimestamp.now();
2022-01-11 21:00:12 +01:00
dg.operationStatus = OperationStatus.Finished;
2021-06-09 15:14:17 +02:00
await tx.depositGroups.put(dg);
}
2023-04-22 14:17:49 +02:00
return computeDepositTransactionStatus(dg);
});
if (!txStatusNew) {
// Doesn't exist anymore!
return OperationAttemptResult.finishedEmpty();
}
// Notify if state transitioned
if (
txStateOld.major !== txStatusNew.major ||
txStateOld.minor !== txStatusNew.minor
) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: txStateOld,
newTxState: txStatusNew,
2021-06-09 15:14:17 +02:00
});
2023-04-22 14:17:49 +02:00
}
// FIXME: consider other cases like aborting, suspend, ...
if (
txStatusNew.major === TransactionMajorState.Pending ||
txStatusNew.major === TransactionMajorState.Aborting
) {
return OperationAttemptResult.pendingEmpty();
} else {
return OperationAttemptResult.finishedEmpty();
}
2021-01-18 23:35:41 +01:00
}
async function getExchangeWireFee(
ws: InternalWalletState,
wireType: string,
baseUrl: string,
time: TalerProtocolTimestamp,
): Promise<WireFee> {
const exchangeDetails = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadOnly(async (tx) => {
const ex = await tx.exchanges.get(baseUrl);
if (!ex || !ex.detailsPointer) return undefined;
return await tx.exchangeDetails.indexes.byPointer.get([
baseUrl,
ex.detailsPointer.currency,
ex.detailsPointer.masterPublicKey,
]);
});
if (!exchangeDetails) {
throw Error(`exchange missing: ${baseUrl}`);
}
const fees = exchangeDetails.wireInfo.feesForType[wireType];
if (!fees || fees.length === 0) {
throw Error(
`exchange ${baseUrl} doesn't have fees for wire type ${wireType}`,
);
}
const fee = fees.find((x) => {
return AbsoluteTime.isBetween(
AbsoluteTime.fromTimestamp(time),
AbsoluteTime.fromTimestamp(x.startStamp),
AbsoluteTime.fromTimestamp(x.endStamp),
);
});
if (!fee) {
throw Error(
`exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`,
);
}
return fee;
}
2023-04-22 14:17:49 +02:00
async function trackDeposit(
2023-01-15 21:48:41 +01:00
ws: InternalWalletState,
depositGroup: DepositGroupRecord,
dp: CoinDepositPermission,
): Promise<TrackTransaction> {
2021-01-18 23:35:41 +01:00
const wireHash = depositGroup.contractTermsRaw.h_wire;
2023-01-15 21:48:41 +01:00
const url = new URL(
`deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
dp.exchange_url,
);
const sigResp = await ws.cryptoApi.signTrackTransaction({
coinPub: dp.coin_pub,
contractTermsHash: depositGroup.contractTermsHash,
merchantPriv: depositGroup.merchantPriv,
merchantPub: depositGroup.merchantPub,
wireHash,
});
url.searchParams.set("merchant_sig", sigResp.sig);
const httpResp = await ws.http.fetch(url.href, { method: "GET" });
2023-04-22 14:17:49 +02:00
logger.trace(`deposits response status: ${httpResp.status}`);
2023-01-15 21:48:41 +01:00
switch (httpResp.status) {
case HttpStatusCode.Accepted: {
const accepted = await readSuccessResponseJsonOrThrow(
httpResp,
codecForTackTransactionAccepted(),
);
return { type: "accepted", ...accepted };
}
case HttpStatusCode.Ok: {
const wired = await readSuccessResponseJsonOrThrow(
httpResp,
codecForTackTransactionWired(),
);
return { type: "wired", ...wired };
}
default: {
throw Error(
`unexpected response from track-transaction (${httpResp.status})`,
);
}
2021-01-18 23:35:41 +01:00
}
}
2023-04-05 17:38:34 +02:00
/**
* Check if creating a deposit group is possible and calculate
* the associated fees.
*
2023-04-05 17:38:34 +02:00
* FIXME: This should be renamed to checkDepositGroup,
* as it doesn't prepare anything
*/
2022-05-03 05:16:03 +02:00
export async function prepareDepositGroup(
ws: InternalWalletState,
req: PrepareDepositRequest,
): Promise<PrepareDepositResponse> {
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) => [x.exchanges, x.exchangeDetails])
2022-05-03 05:16:03 +02:00
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
const details = await getExchangeDetails(tx, e.baseUrl);
if (!details || amount.currency !== details.currency) {
continue;
}
exchangeInfos.push({
master_pub: details.masterPublicKey,
url: e.baseUrl,
});
}
});
const now = AbsoluteTime.now();
const nowRounded = AbsoluteTime.toTimestamp(now);
2022-11-01 11:34:20 +01:00
const contractTerms: MerchantContractTerms = {
2022-05-03 05:16:03 +02:00
auditors: [],
exchanges: exchangeInfos,
amount: req.amount,
max_fee: Amounts.stringify(amount),
max_wire_fee: Amounts.stringify(amount),
wire_method: p.targetType,
timestamp: nowRounded,
merchant_base_url: "",
summary: "",
nonce: "",
wire_transfer_deadline: nowRounded,
order_id: "",
h_wire: "",
pay_deadline: AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
),
merchant: {
name: "(wallet)",
},
merchant_pub: "",
refund_deadline: TalerProtocolTimestamp.zero(),
};
const { h: contractTermsHash } = await ws.cryptoApi.hashString({
str: canonicalJson(contractTerms),
});
const contractData = extractContractData(
contractTerms,
contractTermsHash,
"",
);
const payCoinSel = await selectPayCoinsNew(ws, {
auditors: contractData.allowedAuditors,
exchanges: contractData.allowedExchanges,
2022-05-03 05:16:03 +02:00
wireMethod: contractData.wireMethod,
2022-11-02 17:42:14 +01:00
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
2022-05-03 05:16:03 +02:00
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
2022-11-02 17:42:14 +01:00
wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
2022-05-03 05:16:03 +02:00
prevPayCoins: [],
});
if (payCoinSel.type !== "success") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
},
);
2022-05-03 05:16:03 +02:00
}
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
2022-05-03 05:16:03 +02:00
2023-04-05 17:38:34 +02:00
const effectiveDepositAmount = await getCounterpartyEffectiveDepositAmount(
2022-05-03 05:16:03 +02:00
ws,
p.targetType,
payCoinSel.coinSel,
2022-05-03 05:16:03 +02:00
);
2023-04-06 12:47:34 +02:00
const fees = await getTotalFeesForDepositAmount(
ws,
p.targetType,
amount,
payCoinSel.coinSel,
);
return {
totalDepositCost: Amounts.stringify(totalDepositCost),
effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
2023-04-06 12:47:34 +02:00
fees,
};
2022-05-03 05:16:03 +02:00
}
export function generateDepositGroupTxId(): string {
const depositGroupId = encodeCrock(getRandomBytes(32));
return constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId: depositGroupId,
});
}
2021-01-18 23:35:41 +01:00
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 }[] = [];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
2021-06-09 15:14:17 +02:00
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
const details = await getExchangeDetails(tx, e.baseUrl);
if (!details || amount.currency !== details.currency) {
2021-06-09 15:14:17 +02:00
continue;
}
exchangeInfos.push({
master_pub: details.masterPublicKey,
url: e.baseUrl,
});
}
2021-01-18 23:35:41 +01:00
});
2022-03-18 15:32:41 +01:00
const now = AbsoluteTime.now();
const nowRounded = AbsoluteTime.toTimestamp(now);
2022-03-23 21:24:23 +01:00
const noncePair = await ws.cryptoApi.createEddsaKeypair({});
const merchantPair = await ws.cryptoApi.createEddsaKeypair({});
2021-11-17 10:23:22 +01:00
const wireSalt = encodeCrock(getRandomBytes(16));
2021-01-18 23:35:41 +01:00
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
2022-11-01 11:34:20 +01:00
const contractTerms: MerchantContractTerms = {
2021-01-18 23:35:41 +01:00
auditors: [],
exchanges: exchangeInfos,
amount: req.amount,
max_fee: Amounts.stringify(amount),
max_wire_fee: Amounts.stringify(amount),
wire_method: p.targetType,
2022-03-18 15:32:41 +01:00
timestamp: nowRounded,
2021-01-18 23:35:41 +01:00
merchant_base_url: "",
summary: "",
nonce: noncePair.pub,
2022-03-18 15:32:41 +01:00
wire_transfer_deadline: nowRounded,
2021-01-18 23:35:41 +01:00
order_id: "",
h_wire: wireHash,
2022-03-18 15:32:41 +01:00
pay_deadline: AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
2021-01-18 23:35:41 +01:00
),
merchant: {
name: "(wallet)",
2021-01-18 23:35:41 +01:00
},
merchant_pub: merchantPair.pub,
2022-03-18 15:32:41 +01:00
refund_deadline: TalerProtocolTimestamp.zero(),
2021-01-18 23:35:41 +01:00
};
2022-03-23 21:24:23 +01:00
const { h: contractTermsHash } = await ws.cryptoApi.hashString({
str: canonicalJson(contractTerms),
});
2021-01-18 23:35:41 +01:00
const contractData = extractContractData(
contractTerms,
contractTermsHash,
"",
);
const payCoinSel = await selectPayCoinsNew(ws, {
auditors: contractData.allowedAuditors,
exchanges: contractData.allowedExchanges,
wireMethod: contractData.wireMethod,
2022-11-02 17:42:14 +01:00
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
2022-11-02 17:42:14 +01:00
wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
prevPayCoins: [],
});
if (payCoinSel.type !== "success") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
{
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
},
);
2021-01-18 23:35:41 +01:00
}
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
2021-01-18 23:35:41 +01:00
let depositGroupId: string;
if (req.transactionId) {
const txId = parseTransactionIdentifier(req.transactionId);
if (!txId || txId.tag !== TransactionType.Deposit) {
throw Error("invalid transaction ID");
}
depositGroupId = txId.depositGroupId;
} else {
depositGroupId = encodeCrock(getRandomBytes(32));
}
2021-01-18 23:35:41 +01:00
const counterpartyEffectiveDepositAmount =
2023-04-05 17:38:34 +02:00
await getCounterpartyEffectiveDepositAmount(
ws,
p.targetType,
payCoinSel.coinSel,
);
2021-01-18 23:35:41 +01:00
const depositGroup: DepositGroupRecord = {
contractTermsHash,
contractTermsRaw: contractTerms,
depositGroupId,
noncePriv: noncePair.priv,
noncePub: noncePair.pub,
2022-03-18 15:32:41 +01:00
timestampCreated: AbsoluteTime.toTimestamp(now),
2021-01-18 23:35:41 +01:00
timestampFinished: undefined,
2023-01-15 21:48:41 +01:00
transactionPerCoin: payCoinSel.coinSel.coinPubs.map(
2023-04-22 14:17:49 +02:00
() => DepositElementStatus.Unknown,
2023-01-15 21:48:41 +01:00
),
payCoinSelection: payCoinSel.coinSel,
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
depositedPerCoin: payCoinSel.coinSel.coinPubs.map(() => false),
2021-01-18 23:35:41 +01:00
merchantPriv: merchantPair.priv,
merchantPub: merchantPair.pub,
2022-11-02 17:42:14 +01:00
totalPayCost: Amounts.stringify(totalDepositCost),
2023-04-05 17:38:34 +02:00
effectiveDepositAmount: Amounts.stringify(
counterpartyEffectiveDepositAmount,
2023-04-05 17:38:34 +02:00
),
2021-01-18 23:35:41 +01:00
wire: {
payto_uri: req.depositPaytoUri,
salt: wireSalt,
},
2022-01-11 21:00:12 +01:00
operationStatus: OperationStatus.Pending,
2021-01-18 23:35:41 +01:00
};
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
2023-04-22 14:17:49 +02:00
const newTxState = await ws.db
.mktx((x) => [
x.depositGroups,
x.coins,
x.recoupGroups,
x.denominations,
x.refreshGroups,
x.coinAvailability,
])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
await spendCoins(ws, tx, {
allocationId: transactionId,
coinPubs: payCoinSel.coinSel.coinPubs,
contributions: payCoinSel.coinSel.coinContributions.map((x) =>
2022-11-02 17:42:14 +01:00
Amounts.parseOrThrow(x),
),
refreshReason: RefreshReason.PayDeposit,
});
2021-06-09 15:14:17 +02:00
await tx.depositGroups.put(depositGroup);
2023-04-22 14:17:49 +02:00
return computeDepositTransactionStatus(depositGroup);
2021-06-09 15:14:17 +02:00
});
2021-01-18 23:35:41 +01:00
2023-04-22 14:17:49 +02:00
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: {
major: TransactionMajorState.None,
},
newTxState,
});
return {
depositGroupId,
transactionId,
};
}
2021-12-23 19:17:36 +01:00
/**
2023-04-05 17:38:34 +02:00
* Get the amount that will be deposited on the users bank
* account after depositing, not considering aggregation.
2021-12-23 19:17:36 +01:00
*/
2023-04-05 17:38:34 +02:00
export async function getCounterpartyEffectiveDepositAmount(
2021-12-23 19:17:36 +01:00
ws: InternalWalletState,
wireType: string,
pcs: PayCoinSelection,
): Promise<AmountJson> {
const amt: AmountJson[] = [];
const fees: AmountJson[] = [];
const exchangeSet: Set<string> = new Set();
await ws.db
.mktx((x) => [x.coins, x.denominations, x.exchanges, x.exchangeDetails])
2021-12-23 19:17:36 +01:00
.runReadOnly(async (tx) => {
for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await tx.coins.get(pcs.coinPubs[i]);
if (!coin) {
throw Error("can't calculate deposit amount, coin not found");
}
2022-01-13 22:01:14 +01:00
const denom = await ws.getDenomInfo(
ws,
tx,
2021-12-23 19:17:36 +01:00
coin.exchangeBaseUrl,
coin.denomPubHash,
2022-01-13 22:01:14 +01:00
);
2021-12-23 19:17:36 +01:00
if (!denom) {
throw Error("can't find denomination to calculate deposit amount");
}
2022-11-02 17:42:14 +01:00
amt.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
fees.push(Amounts.parseOrThrow(denom.feeDeposit));
2021-12-23 19:17:36 +01:00
exchangeSet.add(coin.exchangeBaseUrl);
}
for (const exchangeUrl of exchangeSet.values()) {
const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
if (!exchangeDetails) {
continue;
}
// FIXME/NOTE: the line below _likely_ throws exception
// about "find method not found on undefined" when the wireType
// is not supported by the Exchange.
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
2022-03-18 15:32:41 +01:00
return AbsoluteTime.isBetween(
AbsoluteTime.now(),
AbsoluteTime.fromTimestamp(x.startStamp),
AbsoluteTime.fromTimestamp(x.endStamp),
2021-12-23 19:17:36 +01:00
);
})?.wireFee;
if (fee) {
2022-11-02 17:42:14 +01:00
fees.push(Amounts.parseOrThrow(fee));
2021-12-23 19:17:36 +01:00
}
}
});
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
}
2023-04-06 12:47:34 +02:00
/**
* Get the fee amount that will be charged when trying to deposit the
* specified amount using the selected coins and the wire method.
*/
export async function getTotalFeesForDepositAmount(
ws: InternalWalletState,
wireType: string,
total: AmountJson,
pcs: PayCoinSelection,
): Promise<DepositGroupFees> {
const wireFee: AmountJson[] = [];
const coinFee: AmountJson[] = [];
const refreshFee: AmountJson[] = [];
const exchangeSet: Set<string> = new Set();
await ws.db
.mktx((x) => [x.coins, x.denominations, x.exchanges, x.exchangeDetails])
.runReadOnly(async (tx) => {
for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await tx.coins.get(pcs.coinPubs[i]);
if (!coin) {
throw Error("can't calculate deposit amount, coin not found");
}
const denom = await ws.getDenomInfo(
ws,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
if (!denom) {
throw Error("can't find denomination to calculate deposit amount");
}
coinFee.push(Amounts.parseOrThrow(denom.feeDeposit));
exchangeSet.add(coin.exchangeBaseUrl);
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.filter((x) =>
Amounts.isSameCurrency(
DenominationRecord.getValue(x),
pcs.coinContributions[i],
),
);
const amountLeft = Amounts.sub(
denom.value,
pcs.coinContributions[i],
).amount;
2023-04-19 17:42:47 +02:00
const refreshCost = getTotalRefreshCost(
allDenoms,
denom,
amountLeft,
ws.config.testing.denomselAllowLate,
);
2023-04-06 12:47:34 +02:00
refreshFee.push(refreshCost);
}
for (const exchangeUrl of exchangeSet.values()) {
const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
if (!exchangeDetails) {
continue;
}
const fee = exchangeDetails.wireInfo.feesForType[wireType]?.find(
(x) => {
return AbsoluteTime.isBetween(
AbsoluteTime.now(),
AbsoluteTime.fromTimestamp(x.startStamp),
AbsoluteTime.fromTimestamp(x.endStamp),
);
},
)?.wireFee;
if (fee) {
wireFee.push(Amounts.parseOrThrow(fee));
}
}
});
return {
coin: Amounts.stringify(Amounts.sumOrZero(total.currency, coinFee).amount),
wire: Amounts.stringify(Amounts.sumOrZero(total.currency, wireFee).amount),
refresh: Amounts.stringify(
Amounts.sumOrZero(total.currency, refreshFee).amount,
),
};
}