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/>
|
|
|
|
*/
|
|
|
|
|
|
|
|
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,
|
|
|
|
buildCodecForObject,
|
2021-06-17 15:49:05 +02:00
|
|
|
canonicalJson,
|
2021-03-17 17:56:37 +01:00
|
|
|
Codec,
|
2022-03-14 18:31:30 +01:00
|
|
|
codecForDepositSuccess,
|
2021-03-17 17:56:37 +01:00
|
|
|
codecForString,
|
2021-01-18 23:35:41 +01:00
|
|
|
codecForTimestamp,
|
2021-03-17 17:56:37 +01:00
|
|
|
codecOptional,
|
|
|
|
ContractTerms,
|
|
|
|
CreateDepositGroupRequest,
|
|
|
|
CreateDepositGroupResponse,
|
2022-02-21 12:40:51 +01:00
|
|
|
durationFromSpec,
|
|
|
|
encodeCrock,
|
|
|
|
GetFeeForDepositRequest,
|
|
|
|
getRandomBytes,
|
2022-03-14 18:31:30 +01:00
|
|
|
hashWire,
|
2021-06-17 15:49:05 +02:00
|
|
|
Logger,
|
2021-03-17 17:56:37 +01:00
|
|
|
NotificationType,
|
|
|
|
parsePaytoUri,
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerErrorDetail,
|
2022-03-18 15:32:41 +01:00
|
|
|
TalerProtocolTimestamp,
|
2021-03-17 17:56:37 +01:00
|
|
|
TrackDepositGroupRequest,
|
|
|
|
TrackDepositGroupResponse,
|
2022-02-21 12:40:51 +01:00
|
|
|
URL,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2022-01-11 21:00:12 +01:00
|
|
|
import { DepositGroupRecord, OperationStatus } from "../db.js";
|
2021-12-23 19:17:36 +01:00
|
|
|
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
|
2021-06-17 15:49:05 +02:00
|
|
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
|
|
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
|
|
|
import { getExchangeDetails } from "./exchanges.js";
|
2021-01-18 23:35:41 +01:00
|
|
|
import {
|
|
|
|
applyCoinSpend,
|
|
|
|
extractContractData,
|
|
|
|
generateDepositPermissions,
|
2021-03-15 13:43:53 +01:00
|
|
|
getCandidatePayCoins,
|
2021-01-18 23:35:41 +01:00
|
|
|
getTotalPaymentCost,
|
2021-06-14 16:08:58 +02:00
|
|
|
} from "./pay.js";
|
2021-12-23 19:17:36 +01:00
|
|
|
import { getTotalRefreshCost } from "./refresh.js";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { guardOperationException } from "./common.js";
|
2021-01-18 23:35:41 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logger.
|
|
|
|
*/
|
|
|
|
const logger = new Logger("deposits.ts");
|
|
|
|
|
|
|
|
async function resetDepositGroupRetry(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const x = await tx.depositGroups.get(depositGroupId);
|
2021-06-11 11:15:08 +02:00
|
|
|
if (x) {
|
2021-06-09 15:14:17 +02:00
|
|
|
x.retryInfo = initRetryInfo();
|
|
|
|
await tx.depositGroups.put(x);
|
|
|
|
}
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function incrementDepositRetry(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
2022-03-22 21:16:38 +01:00
|
|
|
err: TalerErrorDetail | undefined,
|
2021-01-18 23:35:41 +01:00
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!r.retryInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.retryInfo.retryCounter++;
|
|
|
|
updateRetryInfoTimeout(r.retryInfo);
|
|
|
|
r.lastError = err;
|
|
|
|
await tx.depositGroups.put(r);
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
if (err) {
|
|
|
|
ws.notify({ type: NotificationType.DepositOperationError, error: err });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function processDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
|
|
|
forceNow = false,
|
|
|
|
): Promise<void> {
|
|
|
|
await ws.memoProcessDeposit.memo(depositGroupId, async () => {
|
2022-03-22 21:16:38 +01:00
|
|
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
2021-01-18 23:35:41 +01:00
|
|
|
incrementDepositRetry(ws, depositGroupId, e);
|
|
|
|
return await guardOperationException(
|
|
|
|
async () => await processDepositGroupImpl(ws, depositGroupId, forceNow),
|
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processDepositGroupImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
depositGroupId: string,
|
2022-01-16 21:47:43 +01:00
|
|
|
forceNow = false,
|
2021-01-18 23:35:41 +01:00
|
|
|
): Promise<void> {
|
|
|
|
if (forceNow) {
|
|
|
|
await resetDepositGroupRetry(ws, depositGroupId);
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
const depositGroup = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
}))
|
|
|
|
.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`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (depositGroup.timestampFinished) {
|
|
|
|
logger.trace(`deposit group ${depositGroupId} already finished`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const contractData = extractContractData(
|
|
|
|
depositGroup.contractTermsRaw,
|
|
|
|
depositGroup.contractTermsHash,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
|
|
|
const depositPermissions = await generateDepositPermissions(
|
|
|
|
ws,
|
|
|
|
depositGroup.payCoinSelection,
|
|
|
|
contractData,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (let i = 0; i < depositPermissions.length; i++) {
|
|
|
|
if (depositGroup.depositedPerCoin[i]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const perm = depositPermissions[i];
|
2021-11-27 20:56:58 +01:00
|
|
|
let requestBody: any;
|
2022-02-21 12:40:51 +01:00
|
|
|
requestBody = {
|
|
|
|
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,
|
|
|
|
};
|
2021-08-07 18:02:16 +02:00
|
|
|
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
|
2022-01-13 22:01:14 +01:00
|
|
|
logger.info(`depositing to ${url}`);
|
2021-11-27 20:56:58 +01:00
|
|
|
const httpResp = await ws.http.postJson(url.href, requestBody);
|
2021-01-18 23:35:41 +01:00
|
|
|
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
|
|
|
if (!dg) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
dg.depositedPerCoin[i] = true;
|
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const dg = await tx.depositGroups.get(depositGroupId);
|
2021-01-18 23:35:41 +01:00
|
|
|
if (!dg) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
let allDeposited = true;
|
|
|
|
for (const d of depositGroup.depositedPerCoin) {
|
|
|
|
if (!d) {
|
|
|
|
allDeposited = false;
|
|
|
|
}
|
2021-01-18 23:35:41 +01:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
if (allDeposited) {
|
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-08-07 18:19:04 +02:00
|
|
|
delete dg.lastError;
|
|
|
|
delete dg.retryInfo;
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.depositGroups.put(dg);
|
|
|
|
}
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function trackDepositGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
req: TrackDepositGroupRequest,
|
|
|
|
): Promise<TrackDepositGroupResponse> {
|
|
|
|
const responses: {
|
|
|
|
status: number;
|
|
|
|
body: any;
|
|
|
|
}[] = [];
|
2021-06-09 15:14:17 +02:00
|
|
|
const depositGroup = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.depositGroups.get(req.depositGroupId);
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
if (!depositGroup) {
|
|
|
|
throw Error("deposit group not found");
|
|
|
|
}
|
|
|
|
const contractData = extractContractData(
|
|
|
|
depositGroup.contractTermsRaw,
|
|
|
|
depositGroup.contractTermsHash,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
|
|
|
const depositPermissions = await generateDepositPermissions(
|
|
|
|
ws,
|
|
|
|
depositGroup.payCoinSelection,
|
|
|
|
contractData,
|
|
|
|
);
|
|
|
|
|
|
|
|
const wireHash = depositGroup.contractTermsRaw.h_wire;
|
|
|
|
|
|
|
|
for (const dp of depositPermissions) {
|
|
|
|
const url = new URL(
|
2021-08-07 17:39:50 +02:00
|
|
|
`deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
|
2021-01-18 23:35:41 +01:00
|
|
|
dp.exchange_url,
|
|
|
|
);
|
2022-03-23 21:24:23 +01:00
|
|
|
const sigResp = await ws.cryptoApi.signTrackTransaction({
|
2021-01-18 23:35:41 +01:00
|
|
|
coinPub: dp.coin_pub,
|
|
|
|
contractTermsHash: depositGroup.contractTermsHash,
|
|
|
|
merchantPriv: depositGroup.merchantPriv,
|
|
|
|
merchantPub: depositGroup.merchantPub,
|
|
|
|
wireHash,
|
|
|
|
});
|
2022-03-23 21:24:23 +01:00
|
|
|
url.searchParams.set("merchant_sig", sigResp.sig);
|
2021-01-18 23:35:41 +01:00
|
|
|
const httpResp = await ws.http.get(url.href);
|
|
|
|
const body = await httpResp.json();
|
|
|
|
responses.push({
|
|
|
|
body,
|
|
|
|
status: httpResp.status,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
responses,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-12-23 19:17:36 +01:00
|
|
|
export async function getFeeForDeposit(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
req: GetFeeForDepositRequest,
|
|
|
|
): Promise<DepositFee> {
|
|
|
|
const p = parsePaytoUri(req.depositPaytoUri);
|
|
|
|
if (!p) {
|
|
|
|
throw Error("invalid payto URI");
|
|
|
|
}
|
|
|
|
|
|
|
|
const amount = Amounts.parseOrThrow(req.amount);
|
|
|
|
|
|
|
|
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
|
|
|
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const allExchanges = await tx.exchanges.iter().toArray();
|
|
|
|
for (const e of allExchanges) {
|
|
|
|
const details = await getExchangeDetails(tx, e.baseUrl);
|
2022-01-10 19:08:45 +01:00
|
|
|
if (!details || amount.currency !== details.currency) {
|
2021-12-23 19:17:36 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
exchangeInfos.push({
|
|
|
|
master_pub: details.masterPublicKey,
|
|
|
|
url: e.baseUrl,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-18 15:32:41 +01:00
|
|
|
const timestamp = AbsoluteTime.now();
|
|
|
|
const timestampRound = AbsoluteTime.toTimestamp(timestamp);
|
2021-12-23 19:17:36 +01:00
|
|
|
const contractTerms: ContractTerms = {
|
|
|
|
auditors: [],
|
|
|
|
exchanges: exchangeInfos,
|
|
|
|
amount: req.amount,
|
|
|
|
max_fee: Amounts.stringify(amount),
|
|
|
|
max_wire_fee: Amounts.stringify(amount),
|
|
|
|
wire_method: p.targetType,
|
|
|
|
timestamp: timestampRound,
|
|
|
|
merchant_base_url: "",
|
|
|
|
summary: "",
|
|
|
|
nonce: "",
|
|
|
|
wire_transfer_deadline: timestampRound,
|
|
|
|
order_id: "",
|
|
|
|
h_wire: "",
|
2022-03-18 15:32:41 +01:00
|
|
|
pay_deadline: AbsoluteTime.toTimestamp(
|
|
|
|
AbsoluteTime.addDuration(timestamp, durationFromSpec({ hours: 1 })),
|
2021-12-23 19:17:36 +01:00
|
|
|
),
|
|
|
|
merchant: {
|
|
|
|
name: "",
|
|
|
|
},
|
|
|
|
merchant_pub: "",
|
2022-03-18 15:32:41 +01:00
|
|
|
refund_deadline: TalerProtocolTimestamp.zero(),
|
2021-12-23 19:17:36 +01:00
|
|
|
};
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
const contractData = extractContractData(contractTerms, "", "");
|
2021-12-23 19:17:36 +01:00
|
|
|
|
|
|
|
const candidates = await getCandidatePayCoins(ws, contractData);
|
|
|
|
|
|
|
|
const payCoinSel = selectPayCoins({
|
|
|
|
candidates,
|
|
|
|
contractTermsAmount: contractData.amount,
|
|
|
|
depositFeeLimit: contractData.maxDepositFee,
|
|
|
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
|
|
|
wireFeeLimit: contractData.maxWireFee,
|
|
|
|
prevPayCoins: [],
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!payCoinSel) {
|
|
|
|
throw Error("insufficient funds");
|
|
|
|
}
|
|
|
|
|
|
|
|
return await getTotalFeeForDepositAmount(
|
|
|
|
ws,
|
|
|
|
p.targetType,
|
|
|
|
amount,
|
|
|
|
payCoinSel,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
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) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const allExchanges = await tx.exchanges.iter().toArray();
|
|
|
|
for (const e of allExchanges) {
|
|
|
|
const details = await getExchangeDetails(tx, e.baseUrl);
|
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);
|
|
|
|
const contractTerms: ContractTerms = {
|
|
|
|
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: "",
|
2021-11-27 20:56:58 +01:00
|
|
|
// This is always the v2 wire hash, as we're the "merchant" and support v2.
|
2021-01-18 23:35:41 +01:00
|
|
|
h_wire: wireHash,
|
2021-11-27 20:56:58 +01:00
|
|
|
// Required for older exchanges.
|
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: "",
|
|
|
|
},
|
|
|
|
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,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
|
2021-03-15 13:43:53 +01:00
|
|
|
const candidates = await getCandidatePayCoins(ws, {
|
|
|
|
allowedAuditors: contractData.allowedAuditors,
|
|
|
|
allowedExchanges: contractData.allowedExchanges,
|
|
|
|
amount: contractData.amount,
|
|
|
|
maxDepositFee: contractData.maxDepositFee,
|
|
|
|
maxWireFee: contractData.maxWireFee,
|
|
|
|
timestamp: contractData.timestamp,
|
|
|
|
wireFeeAmortization: contractData.wireFeeAmortization,
|
|
|
|
wireMethod: contractData.wireMethod,
|
|
|
|
});
|
|
|
|
|
|
|
|
const payCoinSel = selectPayCoins({
|
|
|
|
candidates,
|
|
|
|
contractTermsAmount: contractData.amount,
|
|
|
|
depositFeeLimit: contractData.maxDepositFee,
|
|
|
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
|
|
|
wireFeeLimit: contractData.maxWireFee,
|
|
|
|
prevPayCoins: [],
|
|
|
|
});
|
|
|
|
|
2021-01-18 23:35:41 +01:00
|
|
|
if (!payCoinSel) {
|
|
|
|
throw Error("insufficient funds");
|
|
|
|
}
|
|
|
|
|
|
|
|
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
|
|
|
|
|
|
|
const depositGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
|
|
|
|
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
|
|
|
ws,
|
|
|
|
p.targetType,
|
|
|
|
payCoinSel,
|
|
|
|
);
|
|
|
|
|
|
|
|
const depositGroup: DepositGroupRecord = {
|
|
|
|
contractTermsHash,
|
|
|
|
contractTermsRaw: contractTerms,
|
|
|
|
depositGroupId,
|
|
|
|
noncePriv: noncePair.priv,
|
|
|
|
noncePub: noncePair.pub,
|
2022-03-18 15:32:41 +01:00
|
|
|
timestampCreated: AbsoluteTime.toTimestamp(now),
|
2021-01-18 23:35:41 +01:00
|
|
|
timestampFinished: undefined,
|
|
|
|
payCoinSelection: payCoinSel,
|
2021-06-22 18:43:11 +02:00
|
|
|
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
|
|
|
depositedPerCoin: payCoinSel.coinPubs.map(() => false),
|
2021-01-18 23:35:41 +01:00
|
|
|
merchantPriv: merchantPair.priv,
|
|
|
|
merchantPub: merchantPair.pub,
|
|
|
|
totalPayCost: totalDepositCost,
|
|
|
|
effectiveDepositAmount,
|
|
|
|
wire: {
|
|
|
|
payto_uri: req.depositPaytoUri,
|
|
|
|
salt: wireSalt,
|
|
|
|
},
|
2021-06-25 13:27:06 +02:00
|
|
|
retryInfo: initRetryInfo(),
|
2022-01-11 21:00:12 +01:00
|
|
|
operationStatus: OperationStatus.Pending,
|
2021-01-18 23:35:41 +01:00
|
|
|
lastError: undefined,
|
|
|
|
};
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
coins: x.coins,
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
denominations: x.denominations,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
2021-06-22 18:43:11 +02:00
|
|
|
await applyCoinSpend(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
payCoinSel,
|
|
|
|
`deposit-group:${depositGroup.depositGroupId}`,
|
|
|
|
);
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.depositGroups.put(depositGroup);
|
|
|
|
});
|
2021-01-18 23:35:41 +01:00
|
|
|
|
|
|
|
return { depositGroupId };
|
2021-04-07 19:29:51 +02:00
|
|
|
}
|
2021-12-23 19:17:36 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the amount that will be deposited on the merchant's bank
|
|
|
|
* account, not considering aggregation.
|
|
|
|
*/
|
|
|
|
export async function getEffectiveDepositAmount(
|
|
|
|
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) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
denominations: x.denominations,
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: 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");
|
|
|
|
}
|
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");
|
|
|
|
}
|
|
|
|
amt.push(pcs.coinContributions[i]);
|
|
|
|
fees.push(denom.feeDeposit);
|
|
|
|
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) {
|
|
|
|
fees.push(fee);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DepositFee {
|
|
|
|
coin: AmountJson;
|
|
|
|
wire: AmountJson;
|
|
|
|
refresh: AmountJson;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 getTotalFeeForDepositAmount(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
wireType: string,
|
|
|
|
total: AmountJson,
|
|
|
|
pcs: PayCoinSelection,
|
|
|
|
): Promise<DepositFee> {
|
|
|
|
const wireFee: AmountJson[] = [];
|
|
|
|
const coinFee: AmountJson[] = [];
|
|
|
|
const refreshFee: AmountJson[] = [];
|
|
|
|
const exchangeSet: Set<string> = new Set();
|
|
|
|
|
|
|
|
// let acc: AmountJson = Amounts.getZero(total.currency);
|
|
|
|
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
denominations: x.denominations,
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: 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");
|
|
|
|
}
|
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");
|
|
|
|
}
|
|
|
|
// const cc = pcs.coinContributions[i]
|
|
|
|
// acc = Amounts.add(acc, cc).amount
|
|
|
|
coinFee.push(denom.feeDeposit);
|
|
|
|
exchangeSet.add(coin.exchangeBaseUrl);
|
|
|
|
|
|
|
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
|
|
|
.iter(coin.exchangeBaseUrl)
|
|
|
|
.filter((x) =>
|
|
|
|
Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
|
|
|
|
);
|
2022-01-11 21:00:12 +01:00
|
|
|
const amountLeft = Amounts.sub(
|
|
|
|
denom.value,
|
|
|
|
pcs.coinContributions[i],
|
|
|
|
).amount;
|
2021-12-23 19:17:36 +01:00
|
|
|
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
|
|
|
|
refreshFee.push(refreshCost);
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
wireFee.push(fee);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
2022-01-11 21:00:12 +01:00
|
|
|
coin:
|
|
|
|
coinFee.length === 0
|
|
|
|
? Amounts.getZero(total.currency)
|
|
|
|
: Amounts.sum(coinFee).amount,
|
|
|
|
wire:
|
|
|
|
wireFee.length === 0
|
|
|
|
? Amounts.getZero(total.currency)
|
|
|
|
: Amounts.sum(wireFee).amount,
|
|
|
|
refresh:
|
|
|
|
refreshFee.length === 0
|
|
|
|
? Amounts.getZero(total.currency)
|
|
|
|
: Amounts.sum(refreshFee).amount,
|
2021-12-23 19:17:36 +01:00
|
|
|
};
|
|
|
|
}
|