wallet-core: implement insufficient balance details
For now, only for merchant payments
This commit is contained in:
parent
44aaa7a636
commit
92f1b5928c
@ -419,6 +419,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
|||||||
"status",
|
"status",
|
||||||
codecForConstString(PreparePayResultType.InsufficientBalance),
|
codecForConstString(PreparePayResultType.InsufficientBalance),
|
||||||
)
|
)
|
||||||
|
.property("balanceDetails", codecForPayMerchantInsufficientBalanceDetails())
|
||||||
.build("PreparePayResultInsufficientBalance");
|
.build("PreparePayResultInsufficientBalance");
|
||||||
|
|
||||||
export const codecForPreparePayResultAlreadyConfirmed =
|
export const codecForPreparePayResultAlreadyConfirmed =
|
||||||
@ -483,6 +484,7 @@ export interface PreparePayResultInsufficientBalance {
|
|||||||
amountRaw: string;
|
amountRaw: string;
|
||||||
noncePriv: string;
|
noncePriv: string;
|
||||||
talerUri: string;
|
talerUri: string;
|
||||||
|
balanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResultAlreadyConfirmed {
|
export interface PreparePayResultAlreadyConfirmed {
|
||||||
@ -2090,32 +2092,32 @@ export interface PayMerchantInsufficientBalanceDetails {
|
|||||||
/**
|
/**
|
||||||
* Amount requested by the merchant.
|
* Amount requested by the merchant.
|
||||||
*/
|
*/
|
||||||
amountRequested: AmountJson;
|
amountRequested: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Balance of type "available" (see balance.ts for definition).
|
* Balance of type "available" (see balance.ts for definition).
|
||||||
*/
|
*/
|
||||||
balanceAvailable: AmountJson;
|
balanceAvailable: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Balance of type "material" (see balance.ts for definition).
|
* Balance of type "material" (see balance.ts for definition).
|
||||||
*/
|
*/
|
||||||
balanceMaterial: AmountJson;
|
balanceMaterial: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Balance of type "age-acceptable" (see balance.ts for definition).
|
* Balance of type "age-acceptable" (see balance.ts for definition).
|
||||||
*/
|
*/
|
||||||
balanceAgeAcceptable: AmountJson;
|
balanceAgeAcceptable: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Balance of type "merchant-acceptable" (see balance.ts for definition).
|
* Balance of type "merchant-acceptable" (see balance.ts for definition).
|
||||||
*/
|
*/
|
||||||
balanceMechantAcceptable: AmountJson;
|
balanceMerchantAcceptable: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Balance of type "merchant-depositable" (see balance.ts for definition).
|
* Balance of type "merchant-depositable" (see balance.ts for definition).
|
||||||
*/
|
*/
|
||||||
balanceMechantDepositable: AmountJson;
|
balanceMerchantDepositable: AmountString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the payment would succeed without fees
|
* If the payment would succeed without fees
|
||||||
@ -2126,5 +2128,17 @@ export interface PayMerchantInsufficientBalanceDetails {
|
|||||||
* It is not possible to give an exact value here, since it depends
|
* It is not possible to give an exact value here, since it depends
|
||||||
* on the coin selection for the amount that would be additionally withdrawn.
|
* on the coin selection for the amount that would be additionally withdrawn.
|
||||||
*/
|
*/
|
||||||
feeGapEstimate: AmountJson;
|
feeGapEstimate: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codecForPayMerchantInsufficientBalanceDetails =
|
||||||
|
(): Codec<PayMerchantInsufficientBalanceDetails> =>
|
||||||
|
buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
|
||||||
|
.property("amountRequested", codecForAmountString())
|
||||||
|
.property("balanceAgeAcceptable", codecForAmountString())
|
||||||
|
.property("balanceAvailable", codecForAmountString())
|
||||||
|
.property("balanceMaterial", codecForAmountString())
|
||||||
|
.property("balanceMerchantAcceptable", codecForAmountString())
|
||||||
|
.property("balanceMerchantDepositable", codecForAmountString())
|
||||||
|
.property("feeGapEstimate", codecForAmountString())
|
||||||
|
.build("PayMerchantInsufficientBalanceDetails");
|
@ -835,6 +835,14 @@ export enum RefreshOperationStatus {
|
|||||||
FinishedWithError = 51 /* DORMANT_START + 1 */,
|
FinishedWithError = 51 /* DORMANT_START + 1 */,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group of refresh operations. The refreshed coins do not
|
||||||
|
* have to belong to the same exchange, but must have the same
|
||||||
|
* currency.
|
||||||
|
*
|
||||||
|
* FIXME: Should include the currency as a top-level field,
|
||||||
|
* but we need to write a migration for that.
|
||||||
|
*/
|
||||||
export interface RefreshGroupRecord {
|
export interface RefreshGroupRecord {
|
||||||
operationStatus: RefreshOperationStatus;
|
operationStatus: RefreshOperationStatus;
|
||||||
|
|
||||||
@ -847,6 +855,13 @@ export interface RefreshGroupRecord {
|
|||||||
*/
|
*/
|
||||||
refreshGroupId: string;
|
refreshGroupId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currency of this refresh group.
|
||||||
|
*
|
||||||
|
* FIXME: Write a migration to add this to earlier DB versions.
|
||||||
|
*/
|
||||||
|
currency: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reason why this refresh group has been created.
|
* Reason why this refresh group has been created.
|
||||||
*/
|
*/
|
||||||
|
@ -86,6 +86,7 @@ export interface RefreshOperations {
|
|||||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
}>,
|
}>,
|
||||||
|
currency: string,
|
||||||
oldCoinPubs: CoinRefreshRequest[],
|
oldCoinPubs: CoinRefreshRequest[],
|
||||||
reason: RefreshReason,
|
reason: RefreshReason,
|
||||||
): Promise<RefreshGroupId>;
|
): Promise<RefreshGroupId>;
|
||||||
|
@ -778,6 +778,7 @@ export async function importBackup(
|
|||||||
timestampFinished: backupRefreshGroup.timestamp_finish,
|
timestampFinished: backupRefreshGroup.timestamp_finish,
|
||||||
timestampCreated: backupRefreshGroup.timestamp_created,
|
timestampCreated: backupRefreshGroup.timestamp_created,
|
||||||
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||||
|
currency: Amounts.currencyOf(backupRefreshGroup.old_coins[0].input_amount),
|
||||||
reason,
|
reason,
|
||||||
lastErrorPerCoin: {},
|
lastErrorPerCoin: {},
|
||||||
oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
|
oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
* can accept via their supported wire methods.
|
* can accept via their supported wire methods.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
@ -52,10 +51,16 @@ import {
|
|||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
Amounts,
|
Amounts,
|
||||||
Logger,
|
Logger,
|
||||||
|
AuditorHandle,
|
||||||
|
ExchangeHandle,
|
||||||
|
canonicalizeBaseUrl,
|
||||||
|
parsePaytoUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { WalletStoresV1 } from "../db.js";
|
import { AllowedAuditorInfo, AllowedExchangeInfo, RefreshGroupRecord, WalletStoresV1 } from "../db.js";
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -68,6 +73,30 @@ interface WalletBalance {
|
|||||||
pendingOutgoing: AmountJson;
|
pendingOutgoing: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the available amount that the wallet expects to get
|
||||||
|
* out of a refresh group.
|
||||||
|
*/
|
||||||
|
function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
|
||||||
|
// Don't count finished refreshes, since the refresh already resulted
|
||||||
|
// in coins being added to the wallet.
|
||||||
|
let available = Amounts.zeroOfCurrency(r.currency);
|
||||||
|
if (r.timestampFinished) {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < r.oldCoinPubs.length; i++) {
|
||||||
|
const session = r.refreshSessionPerCoin[i];
|
||||||
|
if (session) {
|
||||||
|
// We are always assuming the refresh will succeed, thus we
|
||||||
|
// report the output as available balance.
|
||||||
|
available = Amounts.add(available, session.amountRefreshOutput).amount;
|
||||||
|
} else {
|
||||||
|
available = Amounts.add(available, r.estimatedOutputPerCoin[i]).amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get balance information.
|
* Get balance information.
|
||||||
*/
|
*/
|
||||||
@ -110,33 +139,11 @@ export async function getBalancesInsideTransaction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await tx.refreshGroups.iter().forEach((r) => {
|
await tx.refreshGroups.iter().forEach((r) => {
|
||||||
// Don't count finished refreshes, since the refresh already resulted
|
const b = initBalance(r.currency);
|
||||||
// in coins being added to the wallet.
|
|
||||||
if (r.timestampFinished) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < r.oldCoinPubs.length; i++) {
|
|
||||||
const session = r.refreshSessionPerCoin[i];
|
|
||||||
if (session) {
|
|
||||||
const currency = Amounts.parseOrThrow(
|
|
||||||
session.amountRefreshOutput,
|
|
||||||
).currency;
|
|
||||||
const b = initBalance(currency);
|
|
||||||
// We are always assuming the refresh will succeed, thus we
|
|
||||||
// report the output as available balance.
|
|
||||||
b.available = Amounts.add(
|
b.available = Amounts.add(
|
||||||
b.available,
|
b.available,
|
||||||
session.amountRefreshOutput,
|
computeRefreshGroupAvailableAmount(r),
|
||||||
).amount;
|
).amount;
|
||||||
} else {
|
|
||||||
const currency = Amounts.parseOrThrow(r.inputPerCoin[i]).currency;
|
|
||||||
const b = initBalance(currency);
|
|
||||||
b.available = Amounts.add(
|
|
||||||
b.available,
|
|
||||||
r.estimatedOutputPerCoin[i],
|
|
||||||
).amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.withdrawalGroups.iter().forEach((wds) => {
|
await tx.withdrawalGroups.iter().forEach((wds) => {
|
||||||
@ -194,3 +201,217 @@ export async function getBalances(
|
|||||||
|
|
||||||
return wbal;
|
return wbal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the balance for a particular payment to a particular
|
||||||
|
* merchant.
|
||||||
|
*/
|
||||||
|
export interface MerchantPaymentBalanceDetails {
|
||||||
|
balanceAvailable: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantPaymentRestrictionsForBalance {
|
||||||
|
currency: string;
|
||||||
|
minAge: number;
|
||||||
|
acceptedExchanges: AllowedExchangeInfo[];
|
||||||
|
acceptedAuditors: AllowedAuditorInfo[];
|
||||||
|
acceptedWireMethods: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AcceptableExchanges {
|
||||||
|
/**
|
||||||
|
* Exchanges accepted by the merchant, but wire method might not match.
|
||||||
|
*/
|
||||||
|
acceptableExchanges: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchanges accepted by the merchant, including a matching
|
||||||
|
* wire method, i.e. the merchant can deposit coins there.
|
||||||
|
*/
|
||||||
|
depositableExchanges: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all exchanges that are acceptable for a particular payment.
|
||||||
|
*/
|
||||||
|
export async function getAcceptableExchangeBaseUrls(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: MerchantPaymentRestrictionsForBalance,
|
||||||
|
): Promise<AcceptableExchanges> {
|
||||||
|
const acceptableExchangeUrls = new Set<string>();
|
||||||
|
const depositableExchangeUrls = new Set<string>();
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.exchanges, x.exchangeDetails, x.auditorTrust])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
// FIXME: We should have a DB index to look up all exchanges
|
||||||
|
// for a particular auditor ...
|
||||||
|
|
||||||
|
const canonExchanges = new Set<string>();
|
||||||
|
const canonAuditors = new Set<string>();
|
||||||
|
|
||||||
|
for (const exchangeHandle of req.acceptedExchanges) {
|
||||||
|
const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
|
||||||
|
canonExchanges.add(normUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auditorHandle of req.acceptedAuditors) {
|
||||||
|
const normUrl = canonicalizeBaseUrl(auditorHandle.auditorBaseUrl);
|
||||||
|
canonAuditors.add(normUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.exchanges.iter().forEachAsync(async (exchange) => {
|
||||||
|
const dp = exchange.detailsPointer;
|
||||||
|
if (!dp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { currency, masterPublicKey } = dp;
|
||||||
|
const exchangeDetails = await tx.exchangeDetails.indexes.byPointer.get([
|
||||||
|
exchange.baseUrl,
|
||||||
|
currency,
|
||||||
|
masterPublicKey,
|
||||||
|
]);
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let acceptable = false;
|
||||||
|
|
||||||
|
if (canonExchanges.has(exchange.baseUrl)) {
|
||||||
|
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||||
|
acceptable = true;
|
||||||
|
}
|
||||||
|
for (const exchangeAuditor of exchangeDetails.auditors) {
|
||||||
|
if (canonAuditors.has(exchangeAuditor.auditor_url)) {
|
||||||
|
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||||
|
acceptable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// FIXME: Also consider exchange and auditor public key
|
||||||
|
// instead of just base URLs?
|
||||||
|
|
||||||
|
let wireMethodSupported = false;
|
||||||
|
for (const acc of exchangeDetails.wireInfo.accounts) {
|
||||||
|
const pp = parsePaytoUri(acc.payto_uri);
|
||||||
|
checkLogicInvariant(!!pp);
|
||||||
|
for (const wm of req.acceptedWireMethods) {
|
||||||
|
if (pp.targetType === wm) {
|
||||||
|
wireMethodSupported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (wireMethodSupported) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||||
|
if (wireMethodSupported) {
|
||||||
|
depositableExchangeUrls.add(exchange.baseUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
acceptableExchanges: [...acceptableExchangeUrls],
|
||||||
|
depositableExchanges: [...depositableExchangeUrls],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantPaymentBalanceDetails {
|
||||||
|
/**
|
||||||
|
* Balance of type "available" (see balance.ts for definition).
|
||||||
|
*/
|
||||||
|
balanceAvailable: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance of type "material" (see balance.ts for definition).
|
||||||
|
*/
|
||||||
|
balanceMaterial: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance of type "age-acceptable" (see balance.ts for definition).
|
||||||
|
*/
|
||||||
|
balanceAgeAcceptable: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance of type "merchant-acceptable" (see balance.ts for definition).
|
||||||
|
*/
|
||||||
|
balanceMerchantAcceptable: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance of type "merchant-depositable" (see balance.ts for definition).
|
||||||
|
*/
|
||||||
|
balanceMerchantDepositable: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMerchantPaymentBalanceDetails(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: MerchantPaymentRestrictionsForBalance,
|
||||||
|
): Promise<MerchantPaymentBalanceDetails> {
|
||||||
|
const acceptability = await getAcceptableExchangeBaseUrls(ws, req);
|
||||||
|
|
||||||
|
const d: MerchantPaymentBalanceDetails = {
|
||||||
|
balanceAvailable: Amounts.zeroOfCurrency(req.currency),
|
||||||
|
balanceMaterial: Amounts.zeroOfCurrency(req.currency),
|
||||||
|
balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
|
||||||
|
balanceMerchantAcceptable: Amounts.zeroOfCurrency(req.currency),
|
||||||
|
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wbal = await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.coins,
|
||||||
|
x.coinAvailability,
|
||||||
|
x.refreshGroups,
|
||||||
|
x.purchases,
|
||||||
|
x.withdrawalGroups,
|
||||||
|
])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
await tx.coinAvailability.iter().forEach((ca) => {
|
||||||
|
const singleCoinAmount: AmountJson = {
|
||||||
|
currency: ca.currency,
|
||||||
|
fraction: ca.amountFrac,
|
||||||
|
value: ca.amountVal,
|
||||||
|
};
|
||||||
|
const coinAmount: AmountJson = Amounts.mult(
|
||||||
|
singleCoinAmount,
|
||||||
|
ca.freshCoinCount,
|
||||||
|
).amount;
|
||||||
|
d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
|
||||||
|
d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
|
||||||
|
if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
|
||||||
|
d.balanceAgeAcceptable = Amounts.add(
|
||||||
|
d.balanceAgeAcceptable,
|
||||||
|
coinAmount,
|
||||||
|
).amount;
|
||||||
|
if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
|
||||||
|
d.balanceMerchantAcceptable = Amounts.add(
|
||||||
|
d.balanceMerchantAcceptable,
|
||||||
|
coinAmount,
|
||||||
|
).amount;
|
||||||
|
if (
|
||||||
|
acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)
|
||||||
|
) {
|
||||||
|
d.balanceMerchantDepositable = Amounts.add(
|
||||||
|
d.balanceMerchantDepositable,
|
||||||
|
coinAmount,
|
||||||
|
).amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.refreshGroups.iter().forEach((r) => {
|
||||||
|
d.balanceAvailable = Amounts.add(
|
||||||
|
d.balanceAvailable,
|
||||||
|
computeRefreshGroupAvailableAmount(r),
|
||||||
|
).amount;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
@ -175,9 +175,11 @@ export async function spendCoins(
|
|||||||
await tx.coins.put(coin);
|
await tx.coins.put(coin);
|
||||||
await tx.coinAvailability.put(coinAvailability);
|
await tx.coinAvailability.put(coinAvailability);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.refreshOps.createRefreshGroup(
|
await ws.refreshOps.createRefreshGroup(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
|
Amounts.currencyOf(csi.contributions[0]),
|
||||||
refreshCoinPubs,
|
refreshCoinPubs,
|
||||||
RefreshReason.PayMerchant,
|
RefreshReason.PayMerchant,
|
||||||
);
|
);
|
||||||
|
@ -268,7 +268,7 @@ export async function getFeeForDeposit(
|
|||||||
prevPayCoins: [],
|
prevPayCoins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payCoinSel) {
|
if (payCoinSel.type !== "success") {
|
||||||
throw Error("insufficient funds");
|
throw Error("insufficient funds");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ export async function getFeeForDeposit(
|
|||||||
ws,
|
ws,
|
||||||
p.targetType,
|
p.targetType,
|
||||||
amount,
|
amount,
|
||||||
payCoinSel,
|
payCoinSel.coinSel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,16 +355,16 @@ export async function prepareDepositGroup(
|
|||||||
prevPayCoins: [],
|
prevPayCoins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payCoinSel) {
|
if (payCoinSel.type !== "success") {
|
||||||
throw Error("insufficient funds");
|
throw Error("insufficient funds");
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
|
||||||
|
|
||||||
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
||||||
ws,
|
ws,
|
||||||
p.targetType,
|
p.targetType,
|
||||||
payCoinSel,
|
payCoinSel.coinSel,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -452,18 +452,18 @@ export async function createDepositGroup(
|
|||||||
prevPayCoins: [],
|
prevPayCoins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payCoinSel) {
|
if (payCoinSel.type !== "success") {
|
||||||
throw Error("insufficient funds");
|
throw Error("insufficient funds");
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
|
||||||
|
|
||||||
const depositGroupId = encodeCrock(getRandomBytes(32));
|
const depositGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
||||||
ws,
|
ws,
|
||||||
p.targetType,
|
p.targetType,
|
||||||
payCoinSel,
|
payCoinSel.coinSel,
|
||||||
);
|
);
|
||||||
|
|
||||||
const depositGroup: DepositGroupRecord = {
|
const depositGroup: DepositGroupRecord = {
|
||||||
@ -474,9 +474,9 @@ export async function createDepositGroup(
|
|||||||
noncePub: noncePair.pub,
|
noncePub: noncePair.pub,
|
||||||
timestampCreated: AbsoluteTime.toTimestamp(now),
|
timestampCreated: AbsoluteTime.toTimestamp(now),
|
||||||
timestampFinished: undefined,
|
timestampFinished: undefined,
|
||||||
payCoinSelection: payCoinSel,
|
payCoinSelection: payCoinSel.coinSel,
|
||||||
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
||||||
depositedPerCoin: payCoinSel.coinPubs.map(() => false),
|
depositedPerCoin: payCoinSel.coinSel.coinPubs.map(() => false),
|
||||||
merchantPriv: merchantPair.priv,
|
merchantPriv: merchantPair.priv,
|
||||||
merchantPub: merchantPair.pub,
|
merchantPub: merchantPair.pub,
|
||||||
totalPayCost: Amounts.stringify(totalDepositCost),
|
totalPayCost: Amounts.stringify(totalDepositCost),
|
||||||
@ -500,8 +500,8 @@ export async function createDepositGroup(
|
|||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
await spendCoins(ws, tx, {
|
await spendCoins(ws, tx, {
|
||||||
allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
|
allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
|
||||||
coinPubs: payCoinSel.coinPubs,
|
coinPubs: payCoinSel.coinSel.coinPubs,
|
||||||
contributions: payCoinSel.coinContributions.map((x) =>
|
contributions: payCoinSel.coinSel.coinContributions.map((x) =>
|
||||||
Amounts.parseOrThrow(x),
|
Amounts.parseOrThrow(x),
|
||||||
),
|
),
|
||||||
refreshReason: RefreshReason.PayDeposit,
|
refreshReason: RefreshReason.PayDeposit,
|
||||||
|
@ -73,6 +73,7 @@ import {
|
|||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
constructPayUri,
|
constructPayUri,
|
||||||
|
PayMerchantInsufficientBalanceDetails,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -131,11 +132,12 @@ import {
|
|||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
import { getMerchantPaymentBalanceDetails } from "./balance.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
*/
|
*/
|
||||||
const logger = new Logger("pay.ts");
|
const logger = new Logger("pay-merchant.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the total cost of a payment to the customer.
|
* Compute the total cost of a payment to the customer.
|
||||||
@ -817,7 +819,7 @@ async function handleInsufficientFunds(
|
|||||||
requiredMinimumAge: contractData.minimumAge,
|
requiredMinimumAge: contractData.minimumAge,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (res.type !== "success") {
|
||||||
logger.trace("insufficient funds for coin re-selection");
|
logger.trace("insufficient funds for coin re-selection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -841,8 +843,7 @@ async function handleInsufficientFunds(
|
|||||||
if (!payInfo) {
|
if (!payInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
payInfo.payCoinSelection = res;
|
payInfo.payCoinSelection = res.coinSel;
|
||||||
payInfo.payCoinSelection = res;
|
|
||||||
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
|
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
await spendCoins(ws, tx, {
|
await spendCoins(ws, tx, {
|
||||||
@ -905,6 +906,8 @@ export async function selectCandidates(
|
|||||||
x.coinAvailability,
|
x.coinAvailability,
|
||||||
])
|
])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
|
// FIXME: Use the existing helper (from balance.ts) to
|
||||||
|
// get acceptable exchanges.
|
||||||
const denoms: AvailableDenom[] = [];
|
const denoms: AvailableDenom[] = [];
|
||||||
const exchanges = await tx.exchanges.iter().toArray();
|
const exchanges = await tx.exchanges.iter().toArray();
|
||||||
const wfPerExchange: Record<string, AmountJson> = {};
|
const wfPerExchange: Record<string, AmountJson> = {};
|
||||||
@ -1030,6 +1033,7 @@ export function selectGreedy(
|
|||||||
// Don't use this coin if depositing it is more expensive than
|
// Don't use this coin if depositing it is more expensive than
|
||||||
// the amount it would give the merchant.
|
// the amount it would give the merchant.
|
||||||
if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
|
if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
|
||||||
|
tally.lastDepositFee = Amounts.parseOrThrow(aci.feeDeposit);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1129,6 +1133,13 @@ export function selectForced(
|
|||||||
return selectedDenom;
|
return selectedDenom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SelectPayCoinsResult =
|
||||||
|
| {
|
||||||
|
type: "failure";
|
||||||
|
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||||
|
}
|
||||||
|
| { type: "success"; coinSel: PayCoinSelection };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of candidate coins, select coins to spend under the merchant's
|
* Given a list of candidate coins, select coins to spend under the merchant's
|
||||||
* constraints.
|
* constraints.
|
||||||
@ -1142,7 +1153,7 @@ export function selectForced(
|
|||||||
export async function selectPayCoinsNew(
|
export async function selectPayCoinsNew(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: SelectPayCoinRequestNg,
|
req: SelectPayCoinRequestNg,
|
||||||
): Promise<PayCoinSelection | undefined> {
|
): Promise<SelectPayCoinsResult> {
|
||||||
const {
|
const {
|
||||||
contractTermsAmount,
|
contractTermsAmount,
|
||||||
depositFeeLimit,
|
depositFeeLimit,
|
||||||
@ -1168,6 +1179,7 @@ export async function selectPayCoinsNew(
|
|||||||
customerDepositFees: Amounts.zeroOfCurrency(currency),
|
customerDepositFees: Amounts.zeroOfCurrency(currency),
|
||||||
customerWireFees: Amounts.zeroOfCurrency(currency),
|
customerWireFees: Amounts.zeroOfCurrency(currency),
|
||||||
wireFeeCoveredForExchange: new Set(),
|
wireFeeCoveredForExchange: new Set(),
|
||||||
|
lastDepositFee: Amounts.zeroOfCurrency(currency),
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevPayCoins = req.prevPayCoins ?? [];
|
const prevPayCoins = req.prevPayCoins ?? [];
|
||||||
@ -1207,7 +1219,44 @@ export async function selectPayCoinsNew(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedDenom) {
|
if (!selectedDenom) {
|
||||||
return undefined;
|
const details = await getMerchantPaymentBalanceDetails(ws, {
|
||||||
|
acceptedAuditors: req.auditors,
|
||||||
|
acceptedExchanges: req.exchanges,
|
||||||
|
acceptedWireMethods: [req.wireMethod],
|
||||||
|
currency: Amounts.currencyOf(req.contractTermsAmount),
|
||||||
|
minAge: req.requiredMinimumAge ?? 0,
|
||||||
|
});
|
||||||
|
let feeGapEstimate: AmountJson;
|
||||||
|
if (
|
||||||
|
Amounts.cmp(
|
||||||
|
details.balanceMerchantDepositable,
|
||||||
|
req.contractTermsAmount,
|
||||||
|
) >= 0
|
||||||
|
) {
|
||||||
|
// FIXME: We can probably give a better estimate.
|
||||||
|
feeGapEstimate = Amounts.add(
|
||||||
|
tally.amountPayRemaining,
|
||||||
|
tally.lastDepositFee,
|
||||||
|
).amount;
|
||||||
|
} else {
|
||||||
|
feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "failure",
|
||||||
|
insufficientBalanceDetails: {
|
||||||
|
amountRequested: Amounts.stringify(req.contractTermsAmount),
|
||||||
|
balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
|
||||||
|
balanceAvailable: Amounts.stringify(details.balanceAvailable),
|
||||||
|
balanceMaterial: Amounts.stringify(details.balanceMaterial),
|
||||||
|
balanceMerchantAcceptable: Amounts.stringify(
|
||||||
|
details.balanceMerchantAcceptable,
|
||||||
|
),
|
||||||
|
balanceMerchantDepositable: Amounts.stringify(
|
||||||
|
details.balanceMerchantDepositable,
|
||||||
|
),
|
||||||
|
feeGapEstimate: Amounts.stringify(feeGapEstimate),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSel = selectedDenom;
|
const finalSel = selectedDenom;
|
||||||
@ -1244,11 +1293,14 @@ export async function selectPayCoinsNew(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
type: "success",
|
||||||
|
coinSel: {
|
||||||
paymentAmount: Amounts.stringify(contractTermsAmount),
|
paymentAmount: Amounts.stringify(contractTermsAmount),
|
||||||
coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
|
coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
|
||||||
coinPubs,
|
coinPubs,
|
||||||
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
|
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
|
||||||
customerWireFees: Amounts.stringify(tally.customerWireFees),
|
customerWireFees: Amounts.stringify(tally.customerWireFees),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1318,7 +1370,7 @@ export async function checkPaymentByProposalId(
|
|||||||
wireMethod: contractData.wireMethod,
|
wireMethod: contractData.wireMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (res.type !== "success") {
|
||||||
logger.info("not allowing payment, insufficient coins");
|
logger.info("not allowing payment, insufficient coins");
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
@ -1327,10 +1379,11 @@ export async function checkPaymentByProposalId(
|
|||||||
noncePriv: proposal.noncePriv,
|
noncePriv: proposal.noncePriv,
|
||||||
amountRaw: Amounts.stringify(d.contractData.amount),
|
amountRaw: Amounts.stringify(d.contractData.amount),
|
||||||
talerUri,
|
talerUri,
|
||||||
|
balanceDetails: res.insufficientBalanceDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCost = await getTotalPaymentCost(ws, res);
|
const totalCost = await getTotalPaymentCost(ws, res.coinSel);
|
||||||
logger.trace("costInfo", totalCost);
|
logger.trace("costInfo", totalCost);
|
||||||
logger.trace("coinsForPayment", res);
|
logger.trace("coinsForPayment", res);
|
||||||
|
|
||||||
@ -1340,7 +1393,7 @@ export async function checkPaymentByProposalId(
|
|||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
noncePriv: proposal.noncePriv,
|
noncePriv: proposal.noncePriv,
|
||||||
amountEffective: Amounts.stringify(totalCost),
|
amountEffective: Amounts.stringify(totalCost),
|
||||||
amountRaw: Amounts.stringify(res.paymentAmount),
|
amountRaw: Amounts.stringify(res.coinSel.paymentAmount),
|
||||||
contractTermsHash: d.contractData.contractTermsHash,
|
contractTermsHash: d.contractData.contractTermsHash,
|
||||||
talerUri,
|
talerUri,
|
||||||
};
|
};
|
||||||
@ -1666,9 +1719,9 @@ export async function confirmPay(
|
|||||||
|
|
||||||
const contractData = d.contractData;
|
const contractData = d.contractData;
|
||||||
|
|
||||||
let maybeCoinSelection: PayCoinSelection | undefined = undefined;
|
let selectCoinsResult: SelectPayCoinsResult | undefined = undefined;
|
||||||
|
|
||||||
maybeCoinSelection = await selectPayCoinsNew(ws, {
|
selectCoinsResult = await selectPayCoinsNew(ws, {
|
||||||
auditors: contractData.allowedAuditors,
|
auditors: contractData.allowedAuditors,
|
||||||
exchanges: contractData.allowedExchanges,
|
exchanges: contractData.allowedExchanges,
|
||||||
wireMethod: contractData.wireMethod,
|
wireMethod: contractData.wireMethod,
|
||||||
@ -1681,9 +1734,9 @@ export async function confirmPay(
|
|||||||
forcedSelection: forcedCoinSel,
|
forcedSelection: forcedCoinSel,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace("coin selection result", maybeCoinSelection);
|
logger.trace("coin selection result", selectCoinsResult);
|
||||||
|
|
||||||
if (!maybeCoinSelection) {
|
if (selectCoinsResult.type === "failure") {
|
||||||
// Should not happen, since checkPay should be called first
|
// Should not happen, since checkPay should be called first
|
||||||
// FIXME: Actually, this should be handled gracefully,
|
// FIXME: Actually, this should be handled gracefully,
|
||||||
// and the status should be stored in the DB.
|
// and the status should be stored in the DB.
|
||||||
@ -1691,14 +1744,7 @@ export async function confirmPay(
|
|||||||
throw Error("insufficient balance");
|
throw Error("insufficient balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
const coinSelection = maybeCoinSelection;
|
const coinSelection = selectCoinsResult.coinSel;
|
||||||
|
|
||||||
const depositPermissions = await generateDepositPermissions(
|
|
||||||
ws,
|
|
||||||
coinSelection,
|
|
||||||
d.contractData,
|
|
||||||
);
|
|
||||||
|
|
||||||
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
||||||
|
|
||||||
let sessionId: string | undefined;
|
let sessionId: string | undefined;
|
||||||
@ -2373,6 +2419,7 @@ async function acceptRefunds(
|
|||||||
await createRefreshGroup(
|
await createRefreshGroup(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
|
Amounts.currencyOf(refreshCoinsPubs[0].amount),
|
||||||
refreshCoinsPubs,
|
refreshCoinsPubs,
|
||||||
RefreshReason.Refund,
|
RefreshReason.Refund,
|
||||||
);
|
);
|
||||||
|
@ -429,6 +429,7 @@ export async function processRecoupGroupHandler(
|
|||||||
const refreshGroupId = await createRefreshGroup(
|
const refreshGroupId = await createRefreshGroup(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
|
Amounts.currencyOf(rg2.scheduleRefreshCoins[0].amount),
|
||||||
rg2.scheduleRefreshCoins,
|
rg2.scheduleRefreshCoins,
|
||||||
RefreshReason.Recoup,
|
RefreshReason.Recoup,
|
||||||
);
|
);
|
||||||
|
@ -850,6 +850,7 @@ export async function createRefreshGroup(
|
|||||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
}>,
|
}>,
|
||||||
|
currency: string,
|
||||||
oldCoinPubs: CoinRefreshRequest[],
|
oldCoinPubs: CoinRefreshRequest[],
|
||||||
reason: RefreshReason,
|
reason: RefreshReason,
|
||||||
): Promise<RefreshGroupId> {
|
): Promise<RefreshGroupId> {
|
||||||
@ -934,6 +935,7 @@ export async function createRefreshGroup(
|
|||||||
|
|
||||||
const refreshGroup: RefreshGroupRecord = {
|
const refreshGroup: RefreshGroupRecord = {
|
||||||
operationStatus: RefreshOperationStatus.Pending,
|
operationStatus: RefreshOperationStatus.Pending,
|
||||||
|
currency,
|
||||||
timestampFinished: undefined,
|
timestampFinished: undefined,
|
||||||
statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
|
statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
|
||||||
oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
|
oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
|
||||||
@ -1018,7 +1020,7 @@ export async function autoRefresh(
|
|||||||
])
|
])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const exchange = await tx.exchanges.get(exchangeBaseUrl);
|
const exchange = await tx.exchanges.get(exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchange || !exchange.detailsPointer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const coins = await tx.coins.indexes.byBaseUrl
|
const coins = await tx.coins.indexes.byBaseUrl
|
||||||
@ -1059,6 +1061,7 @@ export async function autoRefresh(
|
|||||||
const res = await createRefreshGroup(
|
const res = await createRefreshGroup(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
|
exchange.detailsPointer?.currency,
|
||||||
refreshCoins,
|
refreshCoins,
|
||||||
RefreshReason.Scheduled,
|
RefreshReason.Scheduled,
|
||||||
);
|
);
|
||||||
|
@ -117,6 +117,8 @@ export interface CoinSelectionTally {
|
|||||||
customerWireFees: AmountJson;
|
customerWireFees: AmountJson;
|
||||||
|
|
||||||
wireFeeCoveredForExchange: Set<string>;
|
wireFeeCoveredForExchange: Set<string>;
|
||||||
|
|
||||||
|
lastDepositFee: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,5 +190,6 @@ export function tallyFees(
|
|||||||
customerDepositFees,
|
customerDepositFees,
|
||||||
customerWireFees,
|
customerWireFees,
|
||||||
wireFeeCoveredForExchange,
|
wireFeeCoveredForExchange,
|
||||||
|
lastDepositFee: feeDeposit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1178,6 +1178,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
}
|
}
|
||||||
case WalletApiOperation.ForceRefresh: {
|
case WalletApiOperation.ForceRefresh: {
|
||||||
const req = codecForForceRefreshRequest().decode(payload);
|
const req = codecForForceRefreshRequest().decode(payload);
|
||||||
|
if (req.coinPubList.length == 0) {
|
||||||
|
throw Error("refusing to create empty refresh group");
|
||||||
|
}
|
||||||
const refreshGroupId = await ws.db
|
const refreshGroupId = await ws.db
|
||||||
.mktx((x) => [
|
.mktx((x) => [
|
||||||
x.refreshGroups,
|
x.refreshGroups,
|
||||||
@ -1207,6 +1210,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
return await createRefreshGroup(
|
return await createRefreshGroup(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
|
Amounts.currencyOf(coinPubs[0].amount),
|
||||||
coinPubs,
|
coinPubs,
|
||||||
RefreshReason.Manual,
|
RefreshReason.Manual,
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user