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,
|
2023-04-24 20:24:23 +02:00
|
|
|
CoinRefreshRequest,
|
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,
|
2023-04-24 20:24:23 +02:00
|
|
|
ExchangeRefundRequest,
|
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,
|
2023-04-22 14:17:49 +02:00
|
|
|
NotificationType,
|
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-22 14:17:49 +02:00
|
|
|
TransactionMajorState,
|
|
|
|
TransactionMinorState,
|
2023-04-05 17:38:34 +02:00
|
|
|
TransactionState,
|
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 {
|
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-23 22:49:42 +02:00
|
|
|
import {
|
2023-04-24 20:24:23 +02:00
|
|
|
createRefreshGroup,
|
2023-04-23 22:49:42 +02:00
|
|
|
DepositOperationStatus,
|
2023-04-24 20:24:23 +02:00
|
|
|
DepositTrackingInfo,
|
2023-04-23 22:49:42 +02:00
|
|
|
getTotalRefreshCost,
|
|
|
|
KycPendingInfo,
|
|
|
|
KycUserType,
|
|
|
|
PendingTaskType,
|
2023-04-24 20:24:23 +02:00
|
|
|
RefreshOperationStatus,
|
2023-04-23 22:49:42 +02:00
|
|
|
} 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";
|
2023-04-23 22:49:42 +02:00
|
|
|
import { OperationAttemptResult } from "../util/retries.js";
|
2023-04-21 22:02:34 +02:00
|
|
|
import { 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";
|
2023-04-21 22:02:34 +02:00
|
|
|
import {
|
|
|
|
constructTransactionIdentifier,
|
|
|
|
parseTransactionIdentifier,
|
2023-04-23 22:49:42 +02:00
|
|
|
stopLongpolling,
|
2023-04-21 22:02:34 +02:00
|
|
|
} from "./transactions.js";
|
2023-04-23 22:49:42 +02:00
|
|
|
import { constructTaskIdentifier } from "../util/retries.js";
|
2023-04-24 20:24:23 +02:00
|
|
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
2021-01-18 23:35:41 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logger.
|
|
|
|
*/
|
|
|
|
const logger = new Logger("deposits.ts");
|
|
|
|
|
2023-04-21 22:02:34 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-04-23 22:49:42 +02:00
|
|
|
case DepositOperationStatus.Finished: {
|
2023-04-05 17:38:34 +02:00
|
|
|
return {
|
2023-04-22 14:17:49 +02:00
|
|
|
major: TransactionMajorState.Done,
|
2023-04-05 17:38:34 +02:00
|
|
|
};
|
|
|
|
}
|
2023-04-23 22:49:42 +02:00
|
|
|
case DepositOperationStatus.Pending: {
|
2023-04-05 17:38:34 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-24 20:24:23 +02:00
|
|
|
logger.info(`num total ${numTotal}`);
|
|
|
|
logger.info(`num deposited ${numDeposited}`);
|
|
|
|
|
2023-04-05 17:38:34 +02:00
|
|
|
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
|
|
|
};
|
|
|
|
}
|
2023-04-23 22:49:42 +02:00
|
|
|
case DepositOperationStatus.Suspended:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Suspended,
|
|
|
|
};
|
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> {
|
2023-04-23 22:49:42 +02:00
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
const retryTag = constructTaskIdentifier({
|
|
|
|
tag: PendingTaskType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
let res = await ws.db
|
|
|
|
.mktx((x) => [x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
logger.warn(
|
|
|
|
`can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
|
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const oldState = computeDepositTransactionStatus(dg);
|
|
|
|
switch (dg.operationStatus) {
|
|
|
|
case DepositOperationStatus.Finished:
|
|
|
|
return undefined;
|
|
|
|
case DepositOperationStatus.Pending: {
|
|
|
|
dg.operationStatus = DepositOperationStatus.Suspended;
|
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
return {
|
|
|
|
oldTxState: oldState,
|
|
|
|
newTxState: computeDepositTransactionStatus(dg),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
case DepositOperationStatus.Suspended:
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
stopLongpolling(ws, retryTag);
|
|
|
|
if (res) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.TransactionStateTransition,
|
|
|
|
transactionId,
|
|
|
|
oldTxState: res.oldTxState,
|
|
|
|
newTxState: res.newTxState,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function resumeDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
let res = await ws.db
|
|
|
|
.mktx((x) => [x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
logger.warn(
|
|
|
|
`can't resume deposit group, depositGroupId=${depositGroupId} not found`,
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const oldState = computeDepositTransactionStatus(dg);
|
|
|
|
switch (dg.operationStatus) {
|
|
|
|
case DepositOperationStatus.Finished:
|
|
|
|
return;
|
|
|
|
case DepositOperationStatus.Pending: {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
case DepositOperationStatus.Suspended:
|
|
|
|
dg.operationStatus = DepositOperationStatus.Pending;
|
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
return {
|
|
|
|
oldTxState: oldState,
|
|
|
|
newTxState: computeDepositTransactionStatus(dg),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
ws.latch.trigger();
|
|
|
|
if (res) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.TransactionStateTransition,
|
|
|
|
transactionId,
|
|
|
|
oldTxState: res.oldTxState,
|
|
|
|
newTxState: res.newTxState,
|
|
|
|
});
|
|
|
|
}
|
2023-04-05 17:38:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function abortDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
|
|
|
): Promise<void> {
|
2023-04-23 22:49:42 +02:00
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
const retryTag = constructTaskIdentifier({
|
|
|
|
tag: PendingTaskType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
let res = await ws.db
|
|
|
|
.mktx((x) => [x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
logger.warn(
|
|
|
|
`can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
|
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const oldState = computeDepositTransactionStatus(dg);
|
|
|
|
switch (dg.operationStatus) {
|
|
|
|
case DepositOperationStatus.Finished:
|
|
|
|
return undefined;
|
|
|
|
case DepositOperationStatus.Pending: {
|
|
|
|
dg.operationStatus = DepositOperationStatus.Aborting;
|
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
return {
|
|
|
|
oldTxState: oldState,
|
|
|
|
newTxState: computeDepositTransactionStatus(dg),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
case DepositOperationStatus.Suspended:
|
|
|
|
// FIXME: Can we abort a suspended transaction?!
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
stopLongpolling(ws, retryTag);
|
|
|
|
// Need to process the operation again.
|
|
|
|
ws.latch.trigger();
|
|
|
|
if (res) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.TransactionStateTransition,
|
|
|
|
transactionId,
|
|
|
|
oldTxState: res.oldTxState,
|
|
|
|
newTxState: res.newTxState,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function deleteDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: boolean,
|
|
|
|
opts: { forced?: boolean } = {},
|
|
|
|
) {
|
2023-04-05 17:38:34 +02:00
|
|
|
throw Error("not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-04-21 22:02:34 +02:00
|
|
|
* 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(
|
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})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-24 20:24:23 +02:00
|
|
|
/**
|
|
|
|
* Check whether the refresh associated with the
|
|
|
|
* aborting deposit group is done.
|
|
|
|
*
|
|
|
|
* If done, mark the deposit transaction as aborted.
|
|
|
|
*
|
|
|
|
* Otherwise continue waiting.
|
|
|
|
*
|
|
|
|
* FIXME: Wait for the refresh group notifications instead of periodically
|
|
|
|
* checking the refresh group status.
|
|
|
|
* FIXME: This is just one transaction, can't we do this in the initial
|
|
|
|
* transaction of processDepositGroup?
|
|
|
|
*/
|
|
|
|
async function waitForRefreshOnDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroup: DepositGroupRecord,
|
|
|
|
): Promise<OperationAttemptResult> {
|
|
|
|
const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
|
|
|
|
checkLogicInvariant(!!abortRefreshGroupId);
|
|
|
|
// FIXME: Emit notification on state transition!
|
|
|
|
const res = await ws.db
|
|
|
|
.mktx((x) => [x.refreshGroups, x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
|
|
|
|
let newOpState: DepositOperationStatus | undefined;
|
|
|
|
if (!refreshGroup) {
|
|
|
|
// Maybe it got manually deleted? Means that we should
|
|
|
|
// just go into aborted.
|
|
|
|
logger.warn("no aborting refresh group found for deposit group");
|
|
|
|
newOpState = DepositOperationStatus.Aborted;
|
|
|
|
} else {
|
|
|
|
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
|
|
|
|
newOpState = DepositOperationStatus.Aborted;
|
|
|
|
} else if (
|
|
|
|
refreshGroup.operationStatus ===
|
|
|
|
RefreshOperationStatus.FinishedWithError
|
|
|
|
) {
|
|
|
|
newOpState = DepositOperationStatus.Aborted;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (newOpState) {
|
|
|
|
const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
|
|
|
|
if (!newDg) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const oldDepositTxStatus = computeDepositTransactionStatus(newDg);
|
|
|
|
newDg.operationStatus = newOpState;
|
|
|
|
const newDepositTxStatus = computeDepositTransactionStatus(newDg);
|
|
|
|
await tx.depositGroups.put(newDg);
|
|
|
|
return { oldDepositTxStatus, newDepositTxStatus };
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (res) {
|
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId: depositGroup.depositGroupId,
|
|
|
|
});
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.TransactionStateTransition,
|
|
|
|
transactionId,
|
|
|
|
oldTxState: res.oldDepositTxStatus,
|
|
|
|
newTxState: res.newDepositTxStatus,
|
|
|
|
});
|
|
|
|
return OperationAttemptResult.pendingEmpty();
|
|
|
|
} else {
|
|
|
|
return OperationAttemptResult.pendingEmpty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function refundDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroup: DepositGroupRecord,
|
|
|
|
): Promise<OperationAttemptResult> {
|
|
|
|
const newTxPerCoin = [...depositGroup.transactionPerCoin];
|
|
|
|
for (let i = 0; i < depositGroup.transactionPerCoin.length; i++) {
|
|
|
|
const st = depositGroup.transactionPerCoin[i];
|
|
|
|
switch (st) {
|
|
|
|
case DepositElementStatus.RefundFailed:
|
|
|
|
case DepositElementStatus.RefundSuccess:
|
|
|
|
break;
|
|
|
|
default: {
|
|
|
|
const coinPub = depositGroup.payCoinSelection.coinPubs[i];
|
|
|
|
const coinExchange = await ws.db
|
|
|
|
.mktx((x) => [x.coins])
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const coinRecord = await tx.coins.get(coinPub);
|
|
|
|
checkDbInvariant(!!coinRecord);
|
|
|
|
return coinRecord.exchangeBaseUrl;
|
|
|
|
});
|
|
|
|
const refundAmount = depositGroup.payCoinSelection.coinContributions[i];
|
|
|
|
// We use a constant refund transaction ID, since there can
|
|
|
|
// only be one refund.
|
|
|
|
const rtid = 1;
|
|
|
|
const sig = await ws.cryptoApi.signRefund({
|
|
|
|
coinPub,
|
|
|
|
contractTermsHash: depositGroup.contractTermsHash,
|
|
|
|
merchantPriv: depositGroup.merchantPriv,
|
|
|
|
merchantPub: depositGroup.merchantPub,
|
|
|
|
refundAmount: refundAmount,
|
|
|
|
rtransactionId: rtid,
|
|
|
|
});
|
|
|
|
const refundReq: ExchangeRefundRequest = {
|
|
|
|
h_contract_terms: depositGroup.contractTermsHash,
|
|
|
|
merchant_pub: depositGroup.merchantPub,
|
|
|
|
merchant_sig: sig.sig,
|
|
|
|
refund_amount: refundAmount,
|
|
|
|
rtransaction_id: rtid,
|
|
|
|
};
|
|
|
|
const refundUrl = new URL(`coins/${coinPub}/refund`, coinExchange);
|
|
|
|
const httpResp = await ws.http.fetch(refundUrl.href, {
|
|
|
|
method: "POST",
|
|
|
|
body: refundReq,
|
|
|
|
});
|
|
|
|
let newStatus: DepositElementStatus;
|
|
|
|
if (httpResp.status === 200) {
|
|
|
|
// FIXME: validate response
|
|
|
|
newStatus = DepositElementStatus.RefundSuccess;
|
|
|
|
} else {
|
|
|
|
// FIXME: Store problem somewhere!
|
|
|
|
newStatus = DepositElementStatus.RefundFailed;
|
|
|
|
}
|
|
|
|
// FIXME: Handle case where refund request needs to be tried again
|
|
|
|
newTxPerCoin[i] = newStatus;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let isDone = true;
|
|
|
|
for (let i = 0; i < newTxPerCoin.length; i++) {
|
|
|
|
if (
|
|
|
|
newTxPerCoin[i] != DepositElementStatus.RefundFailed ||
|
|
|
|
newTxPerCoin[i] != DepositElementStatus.RefundSuccess
|
|
|
|
) {
|
|
|
|
isDone = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const currency = Amounts.currencyOf(depositGroup.totalPayCost);
|
|
|
|
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => [
|
|
|
|
x.depositGroups,
|
|
|
|
x.refreshGroups,
|
|
|
|
x.coins,
|
|
|
|
x.denominations,
|
|
|
|
x.coinAvailability,
|
|
|
|
])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
|
|
|
|
if (!newDg) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
newDg.transactionPerCoin = newTxPerCoin;
|
|
|
|
const refreshCoins: CoinRefreshRequest[] = [];
|
|
|
|
for (let i = 0; i < newTxPerCoin.length; i++) {
|
|
|
|
refreshCoins.push({
|
|
|
|
amount: depositGroup.payCoinSelection.coinContributions[i],
|
|
|
|
coinPub: depositGroup.payCoinSelection.coinPubs[i],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (isDone) {
|
|
|
|
const rgid = await createRefreshGroup(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
currency,
|
|
|
|
refreshCoins,
|
|
|
|
RefreshReason.AbortDeposit,
|
|
|
|
);
|
|
|
|
newDg.abortRefreshGroupId = rgid.refreshGroupId;
|
|
|
|
}
|
|
|
|
await tx.depositGroups.put(newDg);
|
|
|
|
});
|
|
|
|
|
|
|
|
return OperationAttemptResult.pendingEmpty();
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-04-22 14:17:49 +02:00
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
|
|
|
|
const txStateOld = computeDepositTransactionStatus(depositGroup);
|
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
if (depositGroup.operationStatus === DepositOperationStatus.Pending) {
|
|
|
|
const contractData = extractContractData(
|
|
|
|
depositGroup.contractTermsRaw,
|
|
|
|
depositGroup.contractTermsHash,
|
|
|
|
"",
|
|
|
|
);
|
2021-01-18 23:35:41 +01:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
// Check for cancellation before expensive operations.
|
|
|
|
options.cancellationToken?.throwIfCancelled();
|
|
|
|
// FIXME: Cache these!
|
|
|
|
const depositPermissions = await generateDepositPermissions(
|
|
|
|
ws,
|
|
|
|
depositGroup.payCoinSelection,
|
|
|
|
contractData,
|
|
|
|
);
|
2021-01-18 23:35:41 +01:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
for (let i = 0; i < depositPermissions.length; i++) {
|
|
|
|
const perm = depositPermissions[i];
|
|
|
|
|
2023-04-24 20:24:23 +02:00
|
|
|
let didDeposit: boolean = false;
|
2023-04-23 22:49:42 +02: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}`);
|
|
|
|
const httpResp = await ws.http.fetch(url.href, {
|
|
|
|
method: "POST",
|
|
|
|
body: requestBody,
|
|
|
|
cancellationToken: options.cancellationToken,
|
|
|
|
});
|
|
|
|
await readSuccessResponseJsonOrThrow(
|
|
|
|
httpResp,
|
|
|
|
codecForDepositSuccess(),
|
|
|
|
);
|
2023-04-24 20:24:23 +02:00
|
|
|
didDeposit = true;
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
2023-01-15 21:48:41 +01:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
let updatedTxStatus: DepositElementStatus | undefined = undefined;
|
2023-03-29 20:14:02 +02:00
|
|
|
|
2023-04-24 17:13:55 +02:00
|
|
|
let newWiredCoin:
|
2023-04-23 22:49:42 +02:00
|
|
|
| {
|
|
|
|
id: string;
|
2023-04-24 20:24:23 +02:00
|
|
|
value: DepositTrackingInfo;
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
|
|
|
| undefined;
|
|
|
|
|
|
|
|
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) {
|
|
|
|
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 {
|
|
|
|
updatedTxStatus = DepositElementStatus.Accepted;
|
|
|
|
}
|
|
|
|
} else if (track.type === "wired") {
|
|
|
|
updatedTxStatus = DepositElementStatus.Wired;
|
2023-03-29 20:14:02 +02:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
const payto = parsePaytoUri(depositGroup.wire.payto_uri);
|
|
|
|
if (!payto) {
|
|
|
|
throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
|
|
|
|
}
|
2023-01-17 19:59:30 +01:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
const fee = await getExchangeWireFee(
|
2023-01-17 19:59:30 +01:00
|
|
|
ws,
|
2023-04-23 22:49:42 +02:00
|
|
|
payto.targetType,
|
2023-01-17 19:59:30 +01:00
|
|
|
perm.exchange_url,
|
2023-04-23 22:49:42 +02:00
|
|
|
track.execution_time,
|
2023-01-17 19:59:30 +01:00
|
|
|
);
|
2023-04-23 22:49:42 +02:00
|
|
|
const raw = Amounts.parseOrThrow(track.coin_contribution);
|
|
|
|
const wireFee = Amounts.parseOrThrow(fee.wireFee);
|
|
|
|
|
2023-04-24 17:13:55 +02:00
|
|
|
newWiredCoin = {
|
2023-04-23 22:49:42 +02:00
|
|
|
value: {
|
|
|
|
amountRaw: Amounts.stringify(raw),
|
|
|
|
wireFee: Amounts.stringify(wireFee),
|
|
|
|
exchangePub: track.exchange_pub,
|
|
|
|
timestampExecuted: track.execution_time,
|
|
|
|
wireTransferId: track.wtid,
|
|
|
|
},
|
|
|
|
id: track.exchange_sig,
|
|
|
|
};
|
2023-01-17 19:59:30 +01:00
|
|
|
} else {
|
2023-04-23 22:49:42 +02:00
|
|
|
updatedTxStatus = DepositElementStatus.Unknown;
|
2023-01-17 19:59:30 +01:00
|
|
|
}
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
2023-03-29 20:14:02 +02:00
|
|
|
|
2023-04-24 20:24:23 +02:00
|
|
|
if (updatedTxStatus !== undefined || didDeposit) {
|
2023-04-23 22:49:42 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => [x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
return;
|
|
|
|
}
|
2023-04-24 20:24:23 +02:00
|
|
|
if (didDeposit) {
|
|
|
|
dg.depositedPerCoin[i] = didDeposit;
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
|
|
|
if (updatedTxStatus !== undefined) {
|
|
|
|
dg.transactionPerCoin[i] = updatedTxStatus;
|
|
|
|
}
|
2023-04-24 17:13:55 +02:00
|
|
|
if (newWiredCoin) {
|
|
|
|
/**
|
|
|
|
* FIXME: if there is a new wire information from the exchange
|
|
|
|
* it should add up to the previous tracking states.
|
|
|
|
*
|
|
|
|
* This may loose information by overriding prev state.
|
|
|
|
*
|
|
|
|
* And: add checks to integration tests
|
|
|
|
*/
|
2023-04-23 22:49:42 +02:00
|
|
|
if (!dg.trackingState) {
|
|
|
|
dg.trackingState = {};
|
|
|
|
}
|
2023-03-29 20:14:02 +02:00
|
|
|
|
2023-04-24 20:24:23 +02:00
|
|
|
dg.trackingState[newWiredCoin.id] =
|
|
|
|
newWiredCoin.value;
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
});
|
2023-01-17 19:59:30 +01:00
|
|
|
}
|
2023-01-15 21:48:41 +01:00
|
|
|
}
|
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
const txStatusNew = await ws.db
|
|
|
|
.mktx((x) => [x.depositGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
let allDepositedAndWired = true;
|
|
|
|
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
|
|
|
|
if (
|
|
|
|
!depositGroup.depositedPerCoin[i] ||
|
|
|
|
depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
|
|
|
|
) {
|
|
|
|
allDepositedAndWired = false;
|
|
|
|
break;
|
2023-03-29 20:14:02 +02:00
|
|
|
}
|
2023-04-23 22:49:42 +02:00
|
|
|
}
|
|
|
|
if (allDepositedAndWired) {
|
|
|
|
dg.timestampFinished = TalerProtocolTimestamp.now();
|
|
|
|
dg.operationStatus = DepositOperationStatus.Finished;
|
2023-01-15 21:48:41 +01:00
|
|
|
await tx.depositGroups.put(dg);
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
2023-04-23 22:49:42 +02:00
|
|
|
return computeDepositTransactionStatus(dg);
|
|
|
|
});
|
2023-04-22 14:17:49 +02:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
if (!txStatusNew) {
|
|
|
|
// Doesn't exist anymore!
|
|
|
|
return OperationAttemptResult.finishedEmpty();
|
|
|
|
}
|
2023-04-22 14:17:49 +02:00
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
// Notify if state transitioned
|
|
|
|
if (
|
|
|
|
txStateOld.major !== txStatusNew.major ||
|
|
|
|
txStateOld.minor !== txStatusNew.minor
|
|
|
|
) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.TransactionStateTransition,
|
|
|
|
transactionId,
|
|
|
|
oldTxState: txStateOld,
|
|
|
|
newTxState: txStatusNew,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: consider other cases like aborting, suspend, ...
|
|
|
|
if (
|
|
|
|
txStatusNew.major === TransactionMajorState.Pending ||
|
|
|
|
txStatusNew.major === TransactionMajorState.Aborting
|
|
|
|
) {
|
|
|
|
return OperationAttemptResult.pendingEmpty();
|
|
|
|
} else {
|
|
|
|
return OperationAttemptResult.finishedEmpty();
|
|
|
|
}
|
2023-04-22 14:17:49 +02:00
|
|
|
}
|
|
|
|
|
2023-04-23 22:49:42 +02:00
|
|
|
if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
|
2023-04-24 20:24:23 +02:00
|
|
|
const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
|
|
|
|
if (!abortRefreshGroupId) {
|
|
|
|
return refundDepositGroup(ws, depositGroup);
|
|
|
|
}
|
|
|
|
return waitForRefreshOnDepositGroup(ws, depositGroup);
|
2023-04-22 14:17:49 +02:00
|
|
|
}
|
2023-04-23 22:49:42 +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-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);
|
2023-04-21 22:02:34 +02:00
|
|
|
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
|
|
|
/**
|
2023-04-21 22:02: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
|
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
|
|
|
);
|
|
|
|
|
2023-04-06 12:47:34 +02:00
|
|
|
const fees = await getTotalFeesForDepositAmount(
|
|
|
|
ws,
|
|
|
|
p.targetType,
|
|
|
|
amount,
|
|
|
|
payCoinSel.coinSel,
|
|
|
|
);
|
|
|
|
|
2022-10-31 17:08:42 +01:00
|
|
|
return {
|
|
|
|
totalDepositCost: Amounts.stringify(totalDepositCost),
|
|
|
|
effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
|
2023-04-06 12:47:34 +02:00
|
|
|
fees,
|
2022-10-31 17:08:42 +01:00
|
|
|
};
|
2022-05-03 05:16:03 +02:00
|
|
|
}
|
2023-01-18 19:30:48 +01:00
|
|
|
|
2023-04-13 11:26:40 +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
|
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
|
|
|
|
2023-04-21 22:02:34 +02: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
|
|
|
|
2023-04-21 22:02:34 +02: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
|
|
|
),
|
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(
|
2023-04-21 22:02:34 +02:00
|
|
|
counterpartyEffectiveDepositAmount,
|
2023-04-05 17:38:34 +02:00
|
|
|
),
|
2021-01-18 23:35:41 +01:00
|
|
|
wire: {
|
|
|
|
payto_uri: req.depositPaytoUri,
|
|
|
|
salt: wireSalt,
|
|
|
|
},
|
2023-04-23 22:49:42 +02:00
|
|
|
operationStatus: DepositOperationStatus.Pending,
|
2021-01-18 23:35:41 +01:00
|
|
|
};
|
|
|
|
|
2023-04-21 22:02:34 +02:00
|
|
|
const transactionId = constructTransactionIdentifier({
|
|
|
|
tag: TransactionType.Deposit,
|
|
|
|
depositGroupId,
|
|
|
|
});
|
|
|
|
|
2023-04-22 14:17:49 +02:00
|
|
|
const newTxState = 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, {
|
2023-04-21 22:02:34 +02:00
|
|
|
allocationId: transactionId,
|
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);
|
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,
|
|
|
|
});
|
|
|
|
|
2022-09-16 16:06:55 +02:00
|
|
|
return {
|
2023-04-21 22:02:34 +02:00
|
|
|
depositGroupId,
|
|
|
|
transactionId,
|
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;
|
|
|
|
}
|
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,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|