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/>
|
|
|
|
*/
|
|
|
|
|
2022-03-28 23:21:49 +02:00
|
|
|
/**
|
|
|
|
* 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,
|
2021-06-17 15:49:05 +02:00
|
|
|
canonicalJson,
|
2022-03-14 18:31:30 +01:00
|
|
|
codecForDepositSuccess,
|
2023-01-15 21:48:41 +01:00
|
|
|
codecForTackTransactionAccepted,
|
|
|
|
codecForTackTransactionWired,
|
|
|
|
CoinDepositPermission,
|
2021-03-17 17:56:37 +01:00
|
|
|
CreateDepositGroupRequest,
|
|
|
|
CreateDepositGroupResponse,
|
2022-09-21 20:43:12 +02:00
|
|
|
DepositGroupFees,
|
2022-02-21 12:40:51 +01:00
|
|
|
durationFromSpec,
|
|
|
|
encodeCrock,
|
2022-10-31 16:50:54 +01:00
|
|
|
ExchangeDepositRequest,
|
2022-02-21 12:40:51 +01:00
|
|
|
getRandomBytes,
|
2023-01-17 19:59:30 +01:00
|
|
|
hashTruncate32,
|
2022-03-14 18:31:30 +01:00
|
|
|
hashWire,
|
2023-01-15 21:48:41 +01:00
|
|
|
HttpStatusCode,
|
2023-02-12 19:30:59 +01:00
|
|
|
j2s,
|
2021-06-17 15:49:05 +02:00
|
|
|
Logger,
|
2023-01-15 21:48:41 +01:00
|
|
|
MerchantContractTerms,
|
2021-03-17 17:56:37 +01:00
|
|
|
parsePaytoUri,
|
2022-06-10 13:03:47 +02:00
|
|
|
PayCoinSelection,
|
2022-05-03 05:16:03 +02:00
|
|
|
PrepareDepositRequest,
|
|
|
|
PrepareDepositResponse,
|
2022-09-14 20:34:37 +02:00
|
|
|
RefreshReason,
|
2023-01-17 19:59:30 +01:00
|
|
|
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-05 17:38:34 +02:00
|
|
|
TransactionState,
|
|
|
|
TransactionStateInfo,
|
|
|
|
TransactionSubstate,
|
2022-09-16 16:06:55 +02:00
|
|
|
TransactionType,
|
2022-02-21 12:40:51 +01:00
|
|
|
URL,
|
2023-03-29 20:14:02 +02:00
|
|
|
WireFee,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-09-05 18:12:30 +02:00
|
|
|
import {
|
|
|
|
DepositGroupRecord,
|
|
|
|
OperationStatus,
|
2023-01-15 21:48:41 +01:00
|
|
|
TransactionStatus,
|
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";
|
|
|
|
import { KycPendingInfo, KycUserType } from "../index.js";
|
2022-03-28 23:21:49 +02:00
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2023-02-15 23:32:42 +01:00
|
|
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
2022-09-16 17:21:54 +02:00
|
|
|
import { OperationAttemptResult } from "../util/retries.js";
|
2022-10-14 22:47:11 +02:00
|
|
|
import { makeTransactionId, spendCoins } from "./common.js";
|
2021-06-17 15:49:05 +02:00
|
|
|
import { getExchangeDetails } from "./exchanges.js";
|
2021-01-18 23:35:41 +01:00
|
|
|
import {
|
|
|
|
extractContractData,
|
|
|
|
generateDepositPermissions,
|
|
|
|
getTotalPaymentCost,
|
2022-10-08 20:56:57 +02:00
|
|
|
} from "./pay-merchant.js";
|
2023-03-31 17:27:05 +02:00
|
|
|
import { selectPayCoinsNew } from "../util/coinSelection.js";
|
2021-01-18 23:35:41 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logger.
|
|
|
|
*/
|
|
|
|
const logger = new Logger("deposits.ts");
|
|
|
|
|
2023-04-05 17:38:34 +02:00
|
|
|
export async function computeDepositTransactionStatus(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
dg: DepositGroupRecord,
|
|
|
|
): Promise<TransactionStateInfo> {
|
|
|
|
switch (dg.operationStatus) {
|
|
|
|
case OperationStatus.Finished: {
|
|
|
|
return {
|
|
|
|
txState: TransactionState.Done,
|
|
|
|
txSubstate: TransactionSubstate.None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
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]) {
|
|
|
|
case TransactionStatus.KycRequired:
|
|
|
|
numKycRequired++;
|
|
|
|
break;
|
|
|
|
case TransactionStatus.Wired:
|
|
|
|
numWired++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (numKycRequired > 0) {
|
|
|
|
return {
|
|
|
|
txState: TransactionState.Pending,
|
|
|
|
txSubstate: TransactionSubstate.DepositKycRequired,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (numDeposited == numTotal) {
|
|
|
|
return {
|
|
|
|
txState: TransactionState.Pending,
|
|
|
|
txSubstate: TransactionSubstate.DepositPendingTrack,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
txState: TransactionState.Pending,
|
|
|
|
txSubstate: TransactionSubstate.DepositPendingInitial,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
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.
|
|
|
|
*
|
|
|
|
* FIXME: Why does this throw an exception when KYC is required?
|
|
|
|
* Should we not return some proper result record here?
|
|
|
|
*/
|
|
|
|
async function checkDepositKycStatus(
|
2023-02-12 19:30:59 +01:00
|
|
|
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
|
2022-09-13 13:25:41 +02:00
|
|
|
.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
|
|
|
}
|
|
|
|
|
|
|
|
const contractData = extractContractData(
|
|
|
|
depositGroup.contractTermsRaw,
|
|
|
|
depositGroup.contractTermsHash,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
2022-03-28 23:59:16 +02:00
|
|
|
// Check for cancellation before expensive operations.
|
|
|
|
options.cancellationToken?.throwIfCancelled();
|
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
|
|
|
|
|
|
|
let updatedDeposit: boolean | undefined = undefined;
|
|
|
|
|
|
|
|
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-03-29 20:14:02 +02:00
|
|
|
let updatedTxStatus: TransactionStatus | undefined = undefined;
|
|
|
|
type ValueOf<T> = T[keyof T];
|
|
|
|
|
|
|
|
let newWiredTransaction:
|
|
|
|
| {
|
|
|
|
id: string;
|
|
|
|
value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
|
|
|
|
}
|
|
|
|
| undefined;
|
|
|
|
|
2023-01-15 21:48:41 +01:00
|
|
|
if (depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired) {
|
|
|
|
const track = await trackDepositPermission(ws, depositGroup, perm);
|
2023-01-17 19:59:30 +01:00
|
|
|
|
|
|
|
if (track.type === "accepted") {
|
|
|
|
if (!track.kyc_ok && track.requirement_row !== undefined) {
|
|
|
|
updatedTxStatus = TransactionStatus.KycRequired;
|
|
|
|
const { requirement_row: requirementRow } = track;
|
|
|
|
const paytoHash = encodeCrock(
|
|
|
|
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
|
|
|
|
);
|
2023-02-12 19:30:59 +01:00
|
|
|
await checkDepositKycStatus(
|
2023-01-17 19:59:30 +01:00
|
|
|
ws,
|
|
|
|
perm.exchange_url,
|
|
|
|
{ paytoHash, requirementRow },
|
|
|
|
"individual",
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
updatedTxStatus = TransactionStatus.Accepted;
|
|
|
|
}
|
|
|
|
} else if (track.type === "wired") {
|
|
|
|
updatedTxStatus = TransactionStatus.Wired;
|
2023-03-29 20:14:02 +02:00
|
|
|
|
|
|
|
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),
|
2023-03-31 19:50:59 +02:00
|
|
|
wireFee: Amounts.stringify(wireFee),
|
2023-03-29 20:14:02 +02:00
|
|
|
exchangePub: track.exchange_pub,
|
|
|
|
timestampExecuted: track.execution_time,
|
|
|
|
wireTransferId: track.wtid,
|
|
|
|
},
|
|
|
|
id: track.exchange_sig,
|
|
|
|
};
|
2023-01-17 19:59:30 +01:00
|
|
|
} else {
|
|
|
|
updatedTxStatus = TransactionStatus.Unknown;
|
|
|
|
}
|
2023-01-15 21:48:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (updatedTxStatus !== undefined || updatedDeposit !== undefined) {
|
|
|
|
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;
|
|
|
|
}
|
2023-03-29 20:14:02 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.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) {
|
|
|
|
return;
|
|
|
|
}
|
2023-01-15 21:48:41 +01:00
|
|
|
let allDepositedAndWired = true;
|
|
|
|
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
|
|
|
|
if (
|
|
|
|
!depositGroup.depositedPerCoin[i] ||
|
|
|
|
depositGroup.transactionPerCoin[i] !== TransactionStatus.Wired
|
|
|
|
) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
2022-09-05 18:12:30 +02:00
|
|
|
return OperationAttemptResult.finishedEmpty();
|
2021-01-18 23:35:41 +01:00
|
|
|
}
|
|
|
|
|
2023-03-29 20:14:02 +02: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-01-15 21:48:41 +01:00
|
|
|
async function trackDepositPermission(
|
|
|
|
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.get(url.href);
|
|
|
|
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
|
|
|
/**
|
|
|
|
* 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
|
2022-09-13 13:25:41 +02:00
|
|
|
.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,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
2022-09-16 16:20:47 +02:00
|
|
|
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: [],
|
|
|
|
});
|
|
|
|
|
2023-01-05 18:45:49 +01:00
|
|
|
if (payCoinSel.type !== "success") {
|
2023-01-06 10:32:39 +01:00
|
|
|
throw TalerError.fromDetail(
|
|
|
|
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
|
|
|
|
{
|
|
|
|
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
|
|
|
|
},
|
|
|
|
);
|
2022-05-03 05:16:03 +02:00
|
|
|
}
|
|
|
|
|
2023-01-05 18:45:49 +01: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,
|
2023-01-05 18:45:49 +01:00
|
|
|
payCoinSel.coinSel,
|
2022-05-03 05:16:03 +02:00
|
|
|
);
|
|
|
|
|
2022-10-31 17:08:42 +01:00
|
|
|
return {
|
|
|
|
totalDepositCost: Amounts.stringify(totalDepositCost),
|
|
|
|
effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
|
|
|
|
};
|
2022-05-03 05:16:03 +02:00
|
|
|
}
|
2023-01-18 19:30:48 +01:00
|
|
|
|
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
|
2022-09-13 13:25:41 +02:00
|
|
|
.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);
|
2022-01-10 19:08:45 +01:00
|
|
|
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: {
|
2022-03-29 13:47:32 +02:00
|
|
|
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,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
2022-09-16 16:20:47 +02:00
|
|
|
const payCoinSel = await selectPayCoinsNew(ws, {
|
|
|
|
auditors: contractData.allowedAuditors,
|
|
|
|
exchanges: contractData.allowedExchanges,
|
2021-03-15 13:43:53 +01:00
|
|
|
wireMethod: contractData.wireMethod,
|
2022-11-02 17:42:14 +01:00
|
|
|
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
|
|
|
|
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
|
2021-03-15 13:43:53 +01:00
|
|
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
2022-11-02 17:42:14 +01:00
|
|
|
wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
|
2021-03-15 13:43:53 +01:00
|
|
|
prevPayCoins: [],
|
|
|
|
});
|
|
|
|
|
2023-01-05 18:45:49 +01:00
|
|
|
if (payCoinSel.type !== "success") {
|
2023-01-06 10:32:39 +01:00
|
|
|
throw TalerError.fromDetail(
|
|
|
|
TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
|
|
|
|
{
|
|
|
|
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
|
|
|
|
},
|
|
|
|
);
|
2021-01-18 23:35:41 +01:00
|
|
|
}
|
|
|
|
|
2023-01-05 18:45:49 +01:00
|
|
|
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
|
2021-01-18 23:35:41 +01:00
|
|
|
|
|
|
|
const depositGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
|
2023-04-05 17:38:34 +02:00
|
|
|
const countarpartyEffectiveDepositAmount =
|
|
|
|
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(
|
|
|
|
() => TransactionStatus.Unknown,
|
|
|
|
),
|
2023-01-05 18:45:49 +01:00
|
|
|
payCoinSelection: payCoinSel.coinSel,
|
2021-06-22 18:43:11 +02:00
|
|
|
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
2023-01-05 18:45:49 +01:00
|
|
|
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(
|
|
|
|
countarpartyEffectiveDepositAmount,
|
|
|
|
),
|
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
|
|
|
};
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.depositGroups,
|
|
|
|
x.coins,
|
|
|
|
x.recoupGroups,
|
|
|
|
x.denominations,
|
|
|
|
x.refreshGroups,
|
2022-09-16 16:20:47 +02:00
|
|
|
x.coinAvailability,
|
2022-09-13 13:25:41 +02:00
|
|
|
])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
2022-09-14 20:34:37 +02:00
|
|
|
await spendCoins(ws, tx, {
|
2022-10-14 22:47:11 +02:00
|
|
|
allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
|
2023-01-05 18:45:49 +01:00
|
|
|
coinPubs: payCoinSel.coinSel.coinPubs,
|
|
|
|
contributions: payCoinSel.coinSel.coinContributions.map((x) =>
|
2022-11-02 17:42:14 +01:00
|
|
|
Amounts.parseOrThrow(x),
|
|
|
|
),
|
2022-09-14 20:34:37 +02:00
|
|
|
refreshReason: RefreshReason.PayDeposit,
|
|
|
|
});
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.depositGroups.put(depositGroup);
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
|
2022-09-16 16:06:55 +02:00
|
|
|
return {
|
|
|
|
depositGroupId: depositGroupId,
|
2022-10-14 22:47:11 +02:00
|
|
|
transactionId: makeTransactionId(TransactionType.Deposit, depositGroupId),
|
2022-09-16 16:06:55 +02:00
|
|
|
};
|
2021-04-07 19:29:51 +02:00
|
|
|
}
|
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
|
2022-09-13 13:25:41 +02:00
|
|
|
.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;
|
|
|
|
}
|