-cleanup
This commit is contained in:
parent
b91caf977f
commit
374d3498d8
@ -24,7 +24,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { BridgeIDBKeyRange, GlobalIDB } from "@gnu-taler/idb-bridge";
|
import { GlobalIDB } from "@gnu-taler/idb-bridge";
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AgeRestriction,
|
AgeRestriction,
|
||||||
@ -47,7 +47,6 @@ import {
|
|||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
parsePaytoUri,
|
|
||||||
parsePayUri,
|
parsePayUri,
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
@ -75,7 +74,6 @@ import {
|
|||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
WalletStoresV1,
|
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
makeErrorDetail,
|
makeErrorDetail,
|
||||||
@ -88,11 +86,8 @@ import {
|
|||||||
} from "../internal-wallet-state.js";
|
} from "../internal-wallet-state.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import {
|
import {
|
||||||
AvailableCoinInfo,
|
|
||||||
CoinCandidateSelection,
|
|
||||||
CoinSelectionTally,
|
CoinSelectionTally,
|
||||||
PreviousPayCoins,
|
PreviousPayCoins,
|
||||||
selectForcedPayCoins,
|
|
||||||
tallyFees,
|
tallyFees,
|
||||||
} from "../util/coinSelection.js";
|
} from "../util/coinSelection.js";
|
||||||
import {
|
import {
|
||||||
@ -104,11 +99,10 @@ import {
|
|||||||
throwUnexpectedRequestError,
|
throwUnexpectedRequestError,
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
|
||||||
import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
|
import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
|
||||||
import { spendCoins } from "../wallet.js";
|
import { spendCoins } from "../wallet.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
import { getTotalRefreshCost } from "./refresh.js";
|
||||||
import { makeEventId } from "./transactions.js";
|
import { makeEventId } from "./transactions.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,24 +164,6 @@ export async function getTotalPaymentCost(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean {
|
|
||||||
if (denom.isRevoked) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!denom.isOffered) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (coin.status !== CoinStatus.Fresh) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
AbsoluteTime.isExpired(AbsoluteTime.fromTimestamp(denom.stampExpireDeposit))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CoinSelectionRequest {
|
export interface CoinSelectionRequest {
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
@ -898,7 +874,7 @@ export type AvailableDenom = DenominationInfo & {
|
|||||||
numAvailable: number;
|
numAvailable: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function selectCandidates(
|
export async function selectCandidates(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: SelectPayCoinRequestNg,
|
req: SelectPayCoinRequestNg,
|
||||||
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
|
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
|
||||||
@ -937,7 +913,7 @@ async function selectCandidates(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ageLower = 0;
|
let ageLower = 0;
|
||||||
let ageUpper = Number.MAX_SAFE_INTEGER;
|
let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
|
||||||
if (req.requiredMinimumAge) {
|
if (req.requiredMinimumAge) {
|
||||||
ageLower = req.requiredMinimumAge;
|
ageLower = req.requiredMinimumAge;
|
||||||
}
|
}
|
||||||
@ -1522,7 +1498,7 @@ export async function runPayForConfirmPay(
|
|||||||
return {
|
return {
|
||||||
type: ConfirmPayResultType.Done,
|
type: ConfirmPayResultType.Done,
|
||||||
contractTerms: purchase.download.contractTermsRaw,
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
transactionId: makeEventId(TransactionType.Payment, proposalId)
|
transactionId: makeEventId(TransactionType.Payment, proposalId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case OperationAttemptResultType.Error:
|
case OperationAttemptResultType.Error:
|
||||||
|
@ -24,7 +24,8 @@ import {
|
|||||||
AcceptPeerPushPaymentRequest,
|
AcceptPeerPushPaymentRequest,
|
||||||
AcceptPeerPushPaymentResponse,
|
AcceptPeerPushPaymentResponse,
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
AmountJson, Amounts,
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
AmountString,
|
AmountString,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
CheckPeerPullPaymentRequest,
|
CheckPeerPullPaymentRequest,
|
||||||
@ -34,7 +35,8 @@ import {
|
|||||||
Codec,
|
Codec,
|
||||||
codecForAmountString,
|
codecForAmountString,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
codecForExchangeGetContractResponse, constructPayPullUri,
|
codecForExchangeGetContractResponse,
|
||||||
|
constructPayPullUri,
|
||||||
constructPayPushUri,
|
constructPayPushUri,
|
||||||
ContractTermsUtil,
|
ContractTermsUtil,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
@ -58,14 +60,14 @@ import {
|
|||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
WalletAccountMergeFlags
|
WalletAccountMergeFlags,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
MergeReserveInfo,
|
MergeReserveInfo,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
WithdrawalRecordType
|
WithdrawalRecordType,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
@ -339,7 +341,7 @@ export async function initiatePeerToPeerPush(
|
|||||||
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
contractPriv: econtractResp.contractPriv,
|
contractPriv: econtractResp.contractPriv,
|
||||||
}),
|
}),
|
||||||
transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub)
|
transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,9 +554,9 @@ export async function acceptPeerPushPayment(
|
|||||||
return {
|
return {
|
||||||
transactionId: makeEventId(
|
transactionId: makeEventId(
|
||||||
TransactionType.PeerPushCredit,
|
TransactionType.PeerPushCredit,
|
||||||
wg.withdrawalGroupId
|
wg.withdrawalGroupId,
|
||||||
)
|
),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -645,8 +647,8 @@ export async function acceptPeerPullPayment(
|
|||||||
transactionId: makeEventId(
|
transactionId: makeEventId(
|
||||||
TransactionType.PeerPullDebit,
|
TransactionType.PeerPullDebit,
|
||||||
req.peerPullPaymentIncomingId,
|
req.peerPullPaymentIncomingId,
|
||||||
)
|
),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPeerPullPayment(
|
export async function checkPeerPullPayment(
|
||||||
@ -840,7 +842,7 @@ export async function initiatePeerRequestForPay(
|
|||||||
}),
|
}),
|
||||||
transactionId: makeEventId(
|
transactionId: makeEventId(
|
||||||
TransactionType.PeerPullCredit,
|
TransactionType.PeerPullCredit,
|
||||||
wg.withdrawalGroupId
|
wg.withdrawalGroupId,
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,322 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2021 Taler Systems S.A.
|
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
||||||
terms of the GNU General Public License as published by the Free Software
|
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
|
||||||
|
|
||||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
|
||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports.
|
|
||||||
*/
|
|
||||||
import test from "ava";
|
|
||||||
import {
|
|
||||||
AgeRestriction,
|
|
||||||
AmountJson,
|
|
||||||
Amounts,
|
|
||||||
DenomKeyType,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { AvailableCoinInfo, selectPayCoinsLegacy } from "./coinSelection.js";
|
|
||||||
|
|
||||||
function a(x: string): AmountJson {
|
|
||||||
const amt = Amounts.parse(x);
|
|
||||||
if (!amt) {
|
|
||||||
throw Error("invalid amount");
|
|
||||||
}
|
|
||||||
return amt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
|
|
||||||
return {
|
|
||||||
value: a(current),
|
|
||||||
availableAmount: a(current),
|
|
||||||
coinPub: "foobar",
|
|
||||||
denomPub: {
|
|
||||||
cipher: DenomKeyType.Rsa,
|
|
||||||
rsa_public_key: "foobar",
|
|
||||||
age_mask: 0,
|
|
||||||
},
|
|
||||||
feeDeposit: a(feeDeposit),
|
|
||||||
exchangeBaseUrl: "https://example.com/",
|
|
||||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function fakeAciWithAgeRestriction(
|
|
||||||
current: string,
|
|
||||||
feeDeposit: string,
|
|
||||||
): AvailableCoinInfo {
|
|
||||||
return {
|
|
||||||
value: a(current),
|
|
||||||
availableAmount: a(current),
|
|
||||||
coinPub: "foobar",
|
|
||||||
denomPub: {
|
|
||||||
cipher: DenomKeyType.Rsa,
|
|
||||||
rsa_public_key: "foobar",
|
|
||||||
age_mask: 2446657,
|
|
||||||
},
|
|
||||||
feeDeposit: a(feeDeposit),
|
|
||||||
exchangeBaseUrl: "https://example.com/",
|
|
||||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test("it should be able to pay if merchant takes the fees", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.1"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.0"),
|
|
||||||
];
|
|
||||||
acis.forEach((x, i) => (x.coinPub = String(i)));
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.1"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.deepEqual(res.coinPubs, ["1", "0"]);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should take the last two coins if it pays less fees", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.0"),
|
|
||||||
// Merchant covers the fee, this one shouldn't be used
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.0"),
|
|
||||||
];
|
|
||||||
acis.forEach((x, i) => (x.coinPub = String(i)));
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.5"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.deepEqual(res.coinPubs, ["1", "2"]);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should take the last coins if the merchant doest not take all the fee", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
// this coin should be selected instead of previous one with fee
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.0"),
|
|
||||||
];
|
|
||||||
acis.forEach((x, i) => (x.coinPub = String(i)));
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.5"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.deepEqual(res.coinPubs, ["2", "0"]);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should use 3 coins to cover fees and payment", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"), //contributed value 1 (fee by the merchant)
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"), //contributed value .5
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"), //contributed value .5
|
|
||||||
];
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.5"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.true(res.coinPubs.length === 3);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return undefined if there is not enough coins", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
];
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:4.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.2"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
t.true(!res);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return undefined if there is not enough coins (taking into account fees)", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.5"),
|
|
||||||
];
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.2"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
t.true(!res);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should not count into customer fee if merchant can afford it", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.1"),
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.1"),
|
|
||||||
];
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:2.0"),
|
|
||||||
depositFeeLimit: a("EUR:0.2"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
t.truthy(res);
|
|
||||||
t.true(Amounts.cmp(res!.customerDepositFees, "EUR:0.0") === 0);
|
|
||||||
t.true(
|
|
||||||
Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:2.0") === 0,
|
|
||||||
);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should use the coins that spent less relative fee", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.2"),
|
|
||||||
fakeAci("EUR:0.1", "EUR:0.2"),
|
|
||||||
fakeAci("EUR:0.05", "EUR:0.05"),
|
|
||||||
fakeAci("EUR:0.05", "EUR:0.05"),
|
|
||||||
];
|
|
||||||
acis.forEach((x, i) => (x.coinPub = String(i)));
|
|
||||||
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:1.1"),
|
|
||||||
depositFeeLimit: a("EUR:0.4"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.deepEqual(res.coinPubs, ["0", "2", "3"]);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("coin selection 9", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAci("EUR:1.0", "EUR:0.2"),
|
|
||||||
fakeAci("EUR:0.2", "EUR:0.2"),
|
|
||||||
];
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:1.2"),
|
|
||||||
depositFeeLimit: a("EUR:0.4"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
});
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.true(res.coinContributions.length === 2);
|
|
||||||
t.true(
|
|
||||||
Amounts.cmp(Amounts.sum(res.coinContributions).amount, "EUR:1.2") === 0,
|
|
||||||
);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should be able to use unrestricted coins for age restricted contract", (t) => {
|
|
||||||
const acis: AvailableCoinInfo[] = [
|
|
||||||
fakeAciWithAgeRestriction("EUR:1.0", "EUR:0.2"),
|
|
||||||
fakeAciWithAgeRestriction("EUR:0.2", "EUR:0.2"),
|
|
||||||
];
|
|
||||||
const res = selectPayCoinsLegacy({
|
|
||||||
candidates: {
|
|
||||||
candidateCoins: acis,
|
|
||||||
wireFeesPerExchange: {},
|
|
||||||
},
|
|
||||||
contractTermsAmount: a("EUR:1.2"),
|
|
||||||
depositFeeLimit: a("EUR:0.4"),
|
|
||||||
wireFeeLimit: a("EUR:0"),
|
|
||||||
wireFeeAmortization: 1,
|
|
||||||
requiredMinimumAge: 13,
|
|
||||||
});
|
|
||||||
if (!res) {
|
|
||||||
t.fail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t.true(res.coinContributions.length === 2);
|
|
||||||
t.true(
|
|
||||||
Amounts.cmp(Amounts.sum(res.coinContributions).amount, "EUR:1.2") === 0,
|
|
||||||
);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
@ -25,15 +25,11 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
AgeRestriction,
|
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
ForcedCoinSel,
|
|
||||||
Logger,
|
Logger,
|
||||||
PayCoinSelection,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { checkLogicInvariant } from "./invariants.js";
|
|
||||||
|
|
||||||
const logger = new Logger("coinSelection.ts");
|
const logger = new Logger("coinSelection.ts");
|
||||||
|
|
||||||
@ -194,245 +190,3 @@ export function tallyFees(
|
|||||||
wireFeeCoveredForExchange,
|
wireFeeCoveredForExchange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a list of candidate coins, select coins to spend under the merchant's
|
|
||||||
* constraints.
|
|
||||||
*
|
|
||||||
* The prevPayCoins can be specified to "repair" a coin selection
|
|
||||||
* by adding additional coins, after a broken (e.g. double-spent) coin
|
|
||||||
* has been removed from the selection.
|
|
||||||
*
|
|
||||||
* This function is only exported for the sake of unit tests.
|
|
||||||
*/
|
|
||||||
export function selectPayCoinsLegacy(
|
|
||||||
req: SelectPayCoinRequest,
|
|
||||||
): PayCoinSelection | undefined {
|
|
||||||
const {
|
|
||||||
candidates,
|
|
||||||
contractTermsAmount,
|
|
||||||
depositFeeLimit,
|
|
||||||
wireFeeLimit,
|
|
||||||
wireFeeAmortization,
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
if (candidates.candidateCoins.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const coinPubs: string[] = [];
|
|
||||||
const coinContributions: AmountJson[] = [];
|
|
||||||
const currency = contractTermsAmount.currency;
|
|
||||||
|
|
||||||
let tally: CoinSelectionTally = {
|
|
||||||
amountPayRemaining: contractTermsAmount,
|
|
||||||
amountWireFeeLimitRemaining: wireFeeLimit,
|
|
||||||
amountDepositFeeLimitRemaining: depositFeeLimit,
|
|
||||||
customerDepositFees: Amounts.getZero(currency),
|
|
||||||
customerWireFees: Amounts.getZero(currency),
|
|
||||||
wireFeeCoveredForExchange: new Set(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevPayCoins = req.prevPayCoins ?? [];
|
|
||||||
|
|
||||||
// Look at existing pay coin selection and tally up
|
|
||||||
for (const prev of prevPayCoins) {
|
|
||||||
tally = tallyFees(
|
|
||||||
tally,
|
|
||||||
candidates.wireFeesPerExchange,
|
|
||||||
wireFeeAmortization,
|
|
||||||
prev.exchangeBaseUrl,
|
|
||||||
prev.feeDeposit,
|
|
||||||
);
|
|
||||||
tally.amountPayRemaining = Amounts.sub(
|
|
||||||
tally.amountPayRemaining,
|
|
||||||
prev.contribution,
|
|
||||||
).amount;
|
|
||||||
|
|
||||||
coinPubs.push(prev.coinPub);
|
|
||||||
coinContributions.push(prev.contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevCoinPubs = new Set(prevPayCoins.map((x) => x.coinPub));
|
|
||||||
|
|
||||||
// Sort by available amount (descending), deposit fee (ascending) and
|
|
||||||
// denomPub (ascending) if deposit fee is the same
|
|
||||||
// (to guarantee deterministic results)
|
|
||||||
const candidateCoins = [...candidates.candidateCoins].sort(
|
|
||||||
(o1, o2) =>
|
|
||||||
-Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
|
|
||||||
Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
|
|
||||||
DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
|
|
||||||
);
|
|
||||||
|
|
||||||
// FIXME: Here, we should select coins in a smarter way.
|
|
||||||
// Instead of always spending the next-largest coin,
|
|
||||||
// we should try to find the smallest coin that covers the
|
|
||||||
// amount.
|
|
||||||
|
|
||||||
for (const aci of candidateCoins) {
|
|
||||||
// Don't use this coin if depositing it is more expensive than
|
|
||||||
// the amount it would give the merchant.
|
|
||||||
if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Amounts.isZero(tally.amountPayRemaining)) {
|
|
||||||
// We have spent enough!
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same coin can't contribute twice to the same payment,
|
|
||||||
// by a fundamental, intentional limitation of the protocol.
|
|
||||||
if (prevCoinPubs.has(aci.coinPub)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.requiredMinimumAge != null) {
|
|
||||||
const index = AgeRestriction.getAgeGroupIndex(
|
|
||||||
aci.denomPub.age_mask,
|
|
||||||
req.requiredMinimumAge,
|
|
||||||
);
|
|
||||||
// if (!aci.ageCommitmentProof) {
|
|
||||||
// // No age restriction, can't use for this payment
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
if (
|
|
||||||
aci.ageCommitmentProof &&
|
|
||||||
aci.ageCommitmentProof.proof.privateKeys.length < index
|
|
||||||
) {
|
|
||||||
// Available age proofs to low, can't use for this payment
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tally = tallyFees(
|
|
||||||
tally,
|
|
||||||
candidates.wireFeesPerExchange,
|
|
||||||
wireFeeAmortization,
|
|
||||||
aci.exchangeBaseUrl,
|
|
||||||
aci.feeDeposit,
|
|
||||||
);
|
|
||||||
|
|
||||||
let coinSpend = Amounts.max(
|
|
||||||
Amounts.min(tally.amountPayRemaining, aci.availableAmount),
|
|
||||||
aci.feeDeposit,
|
|
||||||
);
|
|
||||||
|
|
||||||
tally.amountPayRemaining = Amounts.sub(
|
|
||||||
tally.amountPayRemaining,
|
|
||||||
coinSpend,
|
|
||||||
).amount;
|
|
||||||
coinPubs.push(aci.coinPub);
|
|
||||||
coinContributions.push(coinSpend);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Amounts.isZero(tally.amountPayRemaining)) {
|
|
||||||
return {
|
|
||||||
paymentAmount: contractTermsAmount,
|
|
||||||
coinContributions,
|
|
||||||
coinPubs,
|
|
||||||
customerDepositFees: tally.customerDepositFees,
|
|
||||||
customerWireFees: tally.customerWireFees,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function selectForcedPayCoins(
|
|
||||||
forcedCoinSel: ForcedCoinSel,
|
|
||||||
req: SelectPayCoinRequest,
|
|
||||||
): PayCoinSelection | undefined {
|
|
||||||
const {
|
|
||||||
candidates,
|
|
||||||
contractTermsAmount,
|
|
||||||
depositFeeLimit,
|
|
||||||
wireFeeLimit,
|
|
||||||
wireFeeAmortization,
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
if (candidates.candidateCoins.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const coinPubs: string[] = [];
|
|
||||||
const coinContributions: AmountJson[] = [];
|
|
||||||
const currency = contractTermsAmount.currency;
|
|
||||||
|
|
||||||
let tally: CoinSelectionTally = {
|
|
||||||
amountPayRemaining: contractTermsAmount,
|
|
||||||
amountWireFeeLimitRemaining: wireFeeLimit,
|
|
||||||
amountDepositFeeLimitRemaining: depositFeeLimit,
|
|
||||||
customerDepositFees: Amounts.getZero(currency),
|
|
||||||
customerWireFees: Amounts.getZero(currency),
|
|
||||||
wireFeeCoveredForExchange: new Set(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Not supported by forced coin selection
|
|
||||||
checkLogicInvariant(!req.prevPayCoins);
|
|
||||||
|
|
||||||
// Sort by available amount (descending), deposit fee (ascending) and
|
|
||||||
// denomPub (ascending) if deposit fee is the same
|
|
||||||
// (to guarantee deterministic results)
|
|
||||||
const candidateCoins = [...candidates.candidateCoins].sort(
|
|
||||||
(o1, o2) =>
|
|
||||||
-Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
|
|
||||||
Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
|
|
||||||
DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
|
|
||||||
);
|
|
||||||
|
|
||||||
// FIXME: Here, we should select coins in a smarter way.
|
|
||||||
// Instead of always spending the next-largest coin,
|
|
||||||
// we should try to find the smallest coin that covers the
|
|
||||||
// amount.
|
|
||||||
|
|
||||||
// Set of spent coin indices from candidate coins
|
|
||||||
const spentSet: Set<number> = new Set();
|
|
||||||
|
|
||||||
for (const forcedCoin of forcedCoinSel.coins) {
|
|
||||||
let aci: AvailableCoinInfo | undefined = undefined;
|
|
||||||
for (let i = 0; i < candidateCoins.length; i++) {
|
|
||||||
if (spentSet.has(i)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
spentSet.add(i);
|
|
||||||
aci = candidateCoins[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!aci) {
|
|
||||||
throw Error("can't find coin for forced coin selection");
|
|
||||||
}
|
|
||||||
|
|
||||||
tally = tallyFees(
|
|
||||||
tally,
|
|
||||||
candidates.wireFeesPerExchange,
|
|
||||||
wireFeeAmortization,
|
|
||||||
aci.exchangeBaseUrl,
|
|
||||||
aci.feeDeposit,
|
|
||||||
);
|
|
||||||
|
|
||||||
let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution);
|
|
||||||
|
|
||||||
tally.amountPayRemaining = Amounts.sub(
|
|
||||||
tally.amountPayRemaining,
|
|
||||||
coinSpend,
|
|
||||||
).amount;
|
|
||||||
coinPubs.push(aci.coinPub);
|
|
||||||
coinContributions.push(coinSpend);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Amounts.isZero(tally.amountPayRemaining)) {
|
|
||||||
return {
|
|
||||||
paymentAmount: contractTermsAmount,
|
|
||||||
coinContributions,
|
|
||||||
coinPubs,
|
|
||||||
customerDepositFees: tally.customerDepositFees,
|
|
||||||
customerWireFees: tally.customerWireFees,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user