model reserve history in the exchange, improve reserve handling logic
This commit is contained in:
parent
1728e5011e
commit
ef0acf06bf
@ -359,8 +359,8 @@ export class CryptoApi {
|
|||||||
return this.doRpc<string>("hashString", 1, str);
|
return this.doRpc<string>("hashString", 1, str);
|
||||||
}
|
}
|
||||||
|
|
||||||
hashDenomPub(denomPub: string): Promise<string> {
|
hashEncoded(encodedBytes: string): Promise<string> {
|
||||||
return this.doRpc<string>("hashDenomPub", 1, denomPub);
|
return this.doRpc<string>("hashEncoded", 1, encodedBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
|
isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
|
||||||
|
@ -49,8 +49,7 @@ import {
|
|||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
} from "../../types/walletTypes";
|
} from "../../types/walletTypes";
|
||||||
import { AmountJson } from "../../util/amounts";
|
import { AmountJson, Amounts } from "../../util/amounts";
|
||||||
import * as Amounts from "../../util/amounts";
|
|
||||||
import * as timer from "../../util/timer";
|
import * as timer from "../../util/timer";
|
||||||
import {
|
import {
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
@ -199,6 +198,7 @@ export class CryptoImplementation {
|
|||||||
denomPubHash: encodeCrock(denomPubHash),
|
denomPubHash: encodeCrock(denomPubHash),
|
||||||
reservePub: encodeCrock(reservePub),
|
reservePub: encodeCrock(reservePub),
|
||||||
withdrawSig: encodeCrock(sig),
|
withdrawSig: encodeCrock(sig),
|
||||||
|
coinEvHash: encodeCrock(evHash),
|
||||||
};
|
};
|
||||||
return planchet;
|
return planchet;
|
||||||
}
|
}
|
||||||
@ -367,7 +367,7 @@ export class CryptoImplementation {
|
|||||||
const s: CoinDepositPermission = {
|
const s: CoinDepositPermission = {
|
||||||
coin_pub: depositInfo.coinPub,
|
coin_pub: depositInfo.coinPub,
|
||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
contribution: Amounts.toString(depositInfo.spendAmount),
|
contribution: Amounts.stringify(depositInfo.spendAmount),
|
||||||
denom_pub: depositInfo.denomPub,
|
denom_pub: depositInfo.denomPub,
|
||||||
exchange_url: depositInfo.exchangeBaseUrl,
|
exchange_url: depositInfo.exchangeBaseUrl,
|
||||||
ub_sig: depositInfo.denomSig,
|
ub_sig: depositInfo.denomSig,
|
||||||
@ -491,10 +491,10 @@ export class CryptoImplementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash a denomination public key.
|
* Hash a crockford encoded value.
|
||||||
*/
|
*/
|
||||||
hashDenomPub(denomPub: string): string {
|
hashEncoded(encodedBytes: string): string {
|
||||||
return encodeCrock(hash(decodeCrock(denomPub)));
|
return encodeCrock(hash(decodeCrock(encodedBytes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
signCoinLink(
|
signCoinLink(
|
||||||
|
@ -35,6 +35,7 @@ import { Database } from "../util/query";
|
|||||||
import { NodeHttpLib } from "./NodeHttpLib";
|
import { NodeHttpLib } from "./NodeHttpLib";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
|
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
|
||||||
|
import { WithdrawalSourceType } from "../types/dbTypes";
|
||||||
|
|
||||||
const logger = new Logger("helpers.ts");
|
const logger = new Logger("helpers.ts");
|
||||||
|
|
||||||
@ -165,8 +166,9 @@ export async function withdrawTestBalance(
|
|||||||
});
|
});
|
||||||
myWallet.addNotificationListener((n) => {
|
myWallet.addNotificationListener((n) => {
|
||||||
if (
|
if (
|
||||||
n.type === NotificationType.ReserveDepleted &&
|
n.type === NotificationType.WithdrawGroupFinished &&
|
||||||
n.reservePub === reservePub
|
n.withdrawalSource.type === WithdrawalSourceType.Reserve &&
|
||||||
|
n.withdrawalSource.reservePub === reservePub
|
||||||
) {
|
) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
|
|||||||
import { MerchantBackendConnection } from "./merchant";
|
import { MerchantBackendConnection } from "./merchant";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { NodeHttpLib } from "./NodeHttpLib";
|
import { NodeHttpLib } from "./NodeHttpLib";
|
||||||
import * as Amounts from "../util/amounts";
|
|
||||||
import { Wallet } from "../wallet";
|
import { Wallet } from "../wallet";
|
||||||
import { Configuration } from "../util/talerconfig";
|
import { Configuration } from "../util/talerconfig";
|
||||||
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
|
|
||||||
const logger = new Logger("integrationtest.ts");
|
const logger = new Logger("integrationtest.ts");
|
||||||
|
|
||||||
@ -127,31 +127,31 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
|
|||||||
await myWallet.runUntilDone();
|
await myWallet.runUntilDone();
|
||||||
|
|
||||||
console.log("withdrawing test balance for refund");
|
console.log("withdrawing test balance for refund");
|
||||||
const withdrawAmountTwo: Amounts.AmountJson = {
|
const withdrawAmountTwo: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 18,
|
value: 18,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
const spendAmountTwo: Amounts.AmountJson = {
|
const spendAmountTwo: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 7,
|
value: 7,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const refundAmount: Amounts.AmountJson = {
|
const refundAmount: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 6,
|
value: 6,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const spendAmountThree: Amounts.AmountJson = {
|
const spendAmountThree: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 3,
|
value: 3,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
await withdrawTestBalance(
|
await withdrawTestBalance(
|
||||||
myWallet,
|
myWallet,
|
||||||
Amounts.toString(withdrawAmountTwo),
|
Amounts.stringify(withdrawAmountTwo),
|
||||||
args.bankBaseUrl,
|
args.bankBaseUrl,
|
||||||
args.exchangeBaseUrl,
|
args.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
@ -162,14 +162,14 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
|
|||||||
let { orderId: refundOrderId } = await makePayment(
|
let { orderId: refundOrderId } = await makePayment(
|
||||||
myWallet,
|
myWallet,
|
||||||
myMerchant,
|
myMerchant,
|
||||||
Amounts.toString(spendAmountTwo),
|
Amounts.stringify(spendAmountTwo),
|
||||||
"order that will be refunded",
|
"order that will be refunded",
|
||||||
);
|
);
|
||||||
|
|
||||||
const refundUri = await myMerchant.refund(
|
const refundUri = await myMerchant.refund(
|
||||||
refundOrderId,
|
refundOrderId,
|
||||||
"test refund",
|
"test refund",
|
||||||
Amounts.toString(refundAmount),
|
Amounts.stringify(refundAmount),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("refund URI", refundUri);
|
console.log("refund URI", refundUri);
|
||||||
@ -182,7 +182,7 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
|
|||||||
await makePayment(
|
await makePayment(
|
||||||
myWallet,
|
myWallet,
|
||||||
myMerchant,
|
myMerchant,
|
||||||
Amounts.toString(spendAmountThree),
|
Amounts.stringify(spendAmountThree),
|
||||||
"payment after refund",
|
"payment after refund",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
logger.info("withdrawing test balance");
|
logger.info("withdrawing test balance");
|
||||||
await withdrawTestBalance(
|
await withdrawTestBalance(
|
||||||
myWallet,
|
myWallet,
|
||||||
Amounts.toString(parsedWithdrawAmount),
|
Amounts.stringify(parsedWithdrawAmount),
|
||||||
bankBaseUrl,
|
bankBaseUrl,
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
@ -258,7 +258,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
await makePayment(
|
await makePayment(
|
||||||
myWallet,
|
myWallet,
|
||||||
myMerchant,
|
myMerchant,
|
||||||
Amounts.toString(parsedSpendAmount),
|
Amounts.stringify(parsedSpendAmount),
|
||||||
"hello world",
|
"hello world",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -266,24 +266,24 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
await myWallet.runUntilDone();
|
await myWallet.runUntilDone();
|
||||||
|
|
||||||
console.log("withdrawing test balance for refund");
|
console.log("withdrawing test balance for refund");
|
||||||
const withdrawAmountTwo: Amounts.AmountJson = {
|
const withdrawAmountTwo: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 18,
|
value: 18,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
const spendAmountTwo: Amounts.AmountJson = {
|
const spendAmountTwo: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 7,
|
value: 7,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const refundAmount: Amounts.AmountJson = {
|
const refundAmount: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 6,
|
value: 6,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const spendAmountThree: Amounts.AmountJson = {
|
const spendAmountThree: AmountJson = {
|
||||||
currency,
|
currency,
|
||||||
value: 3,
|
value: 3,
|
||||||
fraction: 0,
|
fraction: 0,
|
||||||
@ -291,7 +291,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
|
|
||||||
await withdrawTestBalance(
|
await withdrawTestBalance(
|
||||||
myWallet,
|
myWallet,
|
||||||
Amounts.toString(withdrawAmountTwo),
|
Amounts.stringify(withdrawAmountTwo),
|
||||||
bankBaseUrl,
|
bankBaseUrl,
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
@ -302,14 +302,14 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
let { orderId: refundOrderId } = await makePayment(
|
let { orderId: refundOrderId } = await makePayment(
|
||||||
myWallet,
|
myWallet,
|
||||||
myMerchant,
|
myMerchant,
|
||||||
Amounts.toString(spendAmountTwo),
|
Amounts.stringify(spendAmountTwo),
|
||||||
"order that will be refunded",
|
"order that will be refunded",
|
||||||
);
|
);
|
||||||
|
|
||||||
const refundUri = await myMerchant.refund(
|
const refundUri = await myMerchant.refund(
|
||||||
refundOrderId,
|
refundOrderId,
|
||||||
"test refund",
|
"test refund",
|
||||||
Amounts.toString(refundAmount),
|
Amounts.stringify(refundAmount),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("refund URI", refundUri);
|
console.log("refund URI", refundUri);
|
||||||
@ -322,7 +322,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
|
|||||||
await makePayment(
|
await makePayment(
|
||||||
myWallet,
|
myWallet,
|
||||||
myMerchant,
|
myMerchant,
|
||||||
Amounts.toString(spendAmountThree),
|
Amounts.stringify(spendAmountThree),
|
||||||
"payment after refund",
|
"payment after refund",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import qrcodeGenerator = require("qrcode-generator");
|
|||||||
import * as clk from "./clk";
|
import * as clk from "./clk";
|
||||||
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
|
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import { decodeCrock } from "../crypto/talerCrypto";
|
import { decodeCrock } from "../crypto/talerCrypto";
|
||||||
import { OperationFailedAndReportedError } from "../operations/errors";
|
import { OperationFailedAndReportedError } from "../operations/errors";
|
||||||
import { Bank } from "./bank";
|
import { Bank } from "./bank";
|
||||||
@ -190,7 +190,7 @@ walletCli
|
|||||||
} else {
|
} else {
|
||||||
const currencies = Object.keys(balance.byCurrency).sort();
|
const currencies = Object.keys(balance.byCurrency).sort();
|
||||||
for (const c of currencies) {
|
for (const c of currencies) {
|
||||||
console.log(Amounts.toString(balance.byCurrency[c].available));
|
console.log(Amounts.stringify(balance.byCurrency[c].available));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -356,6 +356,32 @@ advancedCli
|
|||||||
fs.writeFileSync(1, decodeCrock(enc.trim()));
|
fs.writeFileSync(1, decodeCrock(enc.trim()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reservesCli = advancedCli.subcommand("reserves", "reserves", {
|
||||||
|
help: "Manage reserves.",
|
||||||
|
});
|
||||||
|
|
||||||
|
reservesCli
|
||||||
|
.subcommand("list", "list", {
|
||||||
|
help: "List reserves.",
|
||||||
|
})
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
const reserves = await wallet.getReserves();
|
||||||
|
console.log(JSON.stringify(reserves, undefined, 2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
reservesCli
|
||||||
|
.subcommand("update", "update", {
|
||||||
|
help: "Update reserve status via exchange.",
|
||||||
|
})
|
||||||
|
.requiredArgument("reservePub", clk.STRING)
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
await wallet.updateReserve(args.update.reservePub);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
advancedCli
|
advancedCli
|
||||||
.subcommand("payPrepare", "pay-prepare", {
|
.subcommand("payPrepare", "pay-prepare", {
|
||||||
help: "Claim an order but don't pay yet.",
|
help: "Claim an order but don't pay yet.",
|
||||||
@ -464,7 +490,7 @@ advancedCli
|
|||||||
console.log(` exchange ${coin.exchangeBaseUrl}`);
|
console.log(` exchange ${coin.exchangeBaseUrl}`);
|
||||||
console.log(` denomPubHash ${coin.denomPubHash}`);
|
console.log(` denomPubHash ${coin.denomPubHash}`);
|
||||||
console.log(
|
console.log(
|
||||||
` remaining amount ${Amounts.toString(coin.currentAmount)}`,
|
` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -106,7 +106,7 @@ export async function getBalancesInsideTransaction(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.withdrawalSession).forEach((wds) => {
|
await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
|
||||||
let w = wds.totalCoinValue;
|
let w = wds.totalCoinValue;
|
||||||
for (let i = 0; i < wds.planchets.length; i++) {
|
for (let i = 0; i < wds.planchets.length; i++) {
|
||||||
if (wds.withdrawn[i]) {
|
if (wds.withdrawn[i]) {
|
||||||
@ -150,7 +150,7 @@ export async function getBalances(
|
|||||||
Stores.refreshGroups,
|
Stores.refreshGroups,
|
||||||
Stores.reserves,
|
Stores.reserves,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
],
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
return getBalancesInsideTransaction(ws, tx);
|
return getBalancesInsideTransaction(ws, tx);
|
||||||
|
@ -53,7 +53,7 @@ async function denominationRecordFromKeys(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
denomIn: Denomination,
|
denomIn: Denomination,
|
||||||
): Promise<DenominationRecord> {
|
): Promise<DenominationRecord> {
|
||||||
const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
|
const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
|
||||||
const d: DenominationRecord = {
|
const d: DenominationRecord = {
|
||||||
denomPub: denomIn.denom_pub,
|
denomPub: denomIn.denom_pub,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
HistoryQuery,
|
HistoryQuery,
|
||||||
@ -42,6 +42,7 @@ import {
|
|||||||
import { assertUnreachable } from "../util/assertUnreachable";
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
import { TransactionHandle, Store } from "../util/query";
|
import { TransactionHandle, Store } from "../util/query";
|
||||||
import { timestampCmp } from "../util/time";
|
import { timestampCmp } from "../util/time";
|
||||||
|
import { summarizeReserveHistory } from "../util/reserveHistoryUtil";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an event ID from the type and the primary key for the event.
|
* Create an event ID from the type and the primary key for the event.
|
||||||
@ -58,7 +59,7 @@ function getOrderShortInfo(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
amount: Amounts.toString(download.contractData.amount),
|
amount: Amounts.stringify(download.contractData.amount),
|
||||||
fulfillmentUrl: download.contractData.fulfillmentUrl,
|
fulfillmentUrl: download.contractData.fulfillmentUrl,
|
||||||
orderId: download.contractData.orderId,
|
orderId: download.contractData.orderId,
|
||||||
merchantBaseUrl: download.contractData.merchantBaseUrl,
|
merchantBaseUrl: download.contractData.merchantBaseUrl,
|
||||||
@ -176,7 +177,7 @@ export async function getHistory(
|
|||||||
Stores.refreshGroups,
|
Stores.refreshGroups,
|
||||||
Stores.reserves,
|
Stores.reserves,
|
||||||
Stores.tips,
|
Stores.tips,
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
Stores.payEvents,
|
Stores.payEvents,
|
||||||
Stores.refundEvents,
|
Stores.refundEvents,
|
||||||
Stores.reserveUpdatedEvents,
|
Stores.reserveUpdatedEvents,
|
||||||
@ -208,7 +209,7 @@ export async function getHistory(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.iter(Stores.withdrawalSession).forEach((wsr) => {
|
tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
|
||||||
if (wsr.timestampFinish) {
|
if (wsr.timestampFinish) {
|
||||||
const cs: PlanchetRecord[] = [];
|
const cs: PlanchetRecord[] = [];
|
||||||
wsr.planchets.forEach((x) => {
|
wsr.planchets.forEach((x) => {
|
||||||
@ -221,7 +222,7 @@ export async function getHistory(
|
|||||||
if (historyQuery?.extraDebug) {
|
if (historyQuery?.extraDebug) {
|
||||||
verboseDetails = {
|
verboseDetails = {
|
||||||
coins: cs.map((x) => ({
|
coins: cs.map((x) => ({
|
||||||
value: Amounts.toString(x.coinValue),
|
value: Amounts.stringify(x.coinValue),
|
||||||
denomPub: x.denomPub,
|
denomPub: x.denomPub,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@ -229,13 +230,13 @@ export async function getHistory(
|
|||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
type: HistoryEventType.Withdrawn,
|
type: HistoryEventType.Withdrawn,
|
||||||
withdrawSessionId: wsr.withdrawSessionId,
|
withdrawalGroupId: wsr.withdrawalGroupId,
|
||||||
eventId: makeEventId(
|
eventId: makeEventId(
|
||||||
HistoryEventType.Withdrawn,
|
HistoryEventType.Withdrawn,
|
||||||
wsr.withdrawSessionId,
|
wsr.withdrawalGroupId,
|
||||||
),
|
),
|
||||||
amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue),
|
amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue),
|
||||||
amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount),
|
amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
|
||||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||||
timestamp: wsr.timestampFinish,
|
timestamp: wsr.timestampFinish,
|
||||||
withdrawalSource: wsr.source,
|
withdrawalSource: wsr.source,
|
||||||
@ -283,7 +284,7 @@ export async function getHistory(
|
|||||||
coins.push({
|
coins.push({
|
||||||
contribution: x.contribution,
|
contribution: x.contribution,
|
||||||
denomPub: c.denomPub,
|
denomPub: c.denomPub,
|
||||||
value: Amounts.toString(d.value),
|
value: Amounts.stringify(d.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
verboseDetails = { coins };
|
verboseDetails = { coins };
|
||||||
@ -301,7 +302,7 @@ export async function getHistory(
|
|||||||
sessionId: pe.sessionId,
|
sessionId: pe.sessionId,
|
||||||
timestamp: pe.timestamp,
|
timestamp: pe.timestamp,
|
||||||
numCoins: purchase.payReq.coins.length,
|
numCoins: purchase.payReq.coins.length,
|
||||||
amountPaidWithFees: Amounts.toString(amountPaidWithFees),
|
amountPaidWithFees: Amounts.stringify(amountPaidWithFees),
|
||||||
verboseDetails,
|
verboseDetails,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -364,7 +365,7 @@ export async function getHistory(
|
|||||||
}
|
}
|
||||||
outputCoins.push({
|
outputCoins.push({
|
||||||
denomPub: d.denomPub,
|
denomPub: d.denomPub,
|
||||||
value: Amounts.toString(d.value),
|
value: Amounts.stringify(d.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -378,8 +379,8 @@ export async function getHistory(
|
|||||||
eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
|
eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
|
||||||
timestamp: rg.timestampFinished,
|
timestamp: rg.timestampFinished,
|
||||||
refreshReason: rg.reason,
|
refreshReason: rg.reason,
|
||||||
amountRefreshedEffective: Amounts.toString(amountRefreshedEffective),
|
amountRefreshedEffective: Amounts.stringify(amountRefreshedEffective),
|
||||||
amountRefreshedRaw: Amounts.toString(amountRefreshedRaw),
|
amountRefreshedRaw: Amounts.stringify(amountRefreshedRaw),
|
||||||
numInputCoins,
|
numInputCoins,
|
||||||
numOutputCoins,
|
numOutputCoins,
|
||||||
numRefreshedInputCoins,
|
numRefreshedInputCoins,
|
||||||
@ -403,21 +404,22 @@ export async function getHistory(
|
|||||||
type: ReserveType.Manual,
|
type: ReserveType.Manual,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const s = summarizeReserveHistory(reserve.reserveTransactions, reserve.currency);
|
||||||
history.push({
|
history.push({
|
||||||
type: HistoryEventType.ReserveBalanceUpdated,
|
type: HistoryEventType.ReserveBalanceUpdated,
|
||||||
eventId: makeEventId(
|
eventId: makeEventId(
|
||||||
HistoryEventType.ReserveBalanceUpdated,
|
HistoryEventType.ReserveBalanceUpdated,
|
||||||
ru.reserveUpdateId,
|
ru.reserveUpdateId,
|
||||||
),
|
),
|
||||||
amountExpected: ru.amountExpected,
|
|
||||||
amountReserveBalance: ru.amountReserveBalance,
|
|
||||||
timestamp: ru.timestamp,
|
timestamp: ru.timestamp,
|
||||||
newHistoryTransactions: ru.newHistoryTransactions,
|
|
||||||
reserveShortInfo: {
|
reserveShortInfo: {
|
||||||
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
||||||
reserveCreationDetail,
|
reserveCreationDetail,
|
||||||
reservePub: reserve.reservePub,
|
reservePub: reserve.reservePub,
|
||||||
},
|
},
|
||||||
|
reserveAwaitedAmount: Amounts.stringify(s.awaitedReserveAmount),
|
||||||
|
reserveBalance: Amounts.stringify(s.computedReserveBalance),
|
||||||
|
reserveUnclaimedAmount: Amounts.stringify(s.unclaimedReserveAmount),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -428,7 +430,7 @@ export async function getHistory(
|
|||||||
eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
|
eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
|
||||||
timestamp: tip.acceptedTimestamp,
|
timestamp: tip.acceptedTimestamp,
|
||||||
tipId: tip.tipId,
|
tipId: tip.tipId,
|
||||||
tipAmountRaw: Amounts.toString(tip.amount),
|
tipAmountRaw: Amounts.stringify(tip.amount),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -488,9 +490,9 @@ export async function getHistory(
|
|||||||
refundGroupId: re.refundGroupId,
|
refundGroupId: re.refundGroupId,
|
||||||
orderShortInfo,
|
orderShortInfo,
|
||||||
timestamp: re.timestamp,
|
timestamp: re.timestamp,
|
||||||
amountRefundedEffective: Amounts.toString(amountRefundedEffective),
|
amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
|
||||||
amountRefundedRaw: Amounts.toString(amountRefundedRaw),
|
amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
|
||||||
amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
|
amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -499,7 +501,7 @@ export async function getHistory(
|
|||||||
let verboseDetails: any = undefined;
|
let verboseDetails: any = undefined;
|
||||||
if (historyQuery?.extraDebug) {
|
if (historyQuery?.extraDebug) {
|
||||||
verboseDetails = {
|
verboseDetails = {
|
||||||
oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.toString),
|
oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.stringify),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +243,7 @@ async function gatherWithdrawalPending(
|
|||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue: boolean = false,
|
onlyDue: boolean = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.withdrawalSession).forEach((wsr) => {
|
await tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
|
||||||
if (wsr.timestampFinish) {
|
if (wsr.timestampFinish) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -266,7 +266,8 @@ async function gatherWithdrawalPending(
|
|||||||
numCoinsTotal,
|
numCoinsTotal,
|
||||||
numCoinsWithdrawn,
|
numCoinsWithdrawn,
|
||||||
source: wsr.source,
|
source: wsr.source,
|
||||||
withdrawSessionId: wsr.withdrawSessionId,
|
withdrawalGroupId: wsr.withdrawalGroupId,
|
||||||
|
lastError: wsr.lastError,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -444,7 +445,7 @@ export async function getPendingOperations(
|
|||||||
Stores.reserves,
|
Stores.reserves,
|
||||||
Stores.refreshGroups,
|
Stores.refreshGroups,
|
||||||
Stores.coins,
|
Stores.coins,
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
Stores.proposals,
|
Stores.proposals,
|
||||||
Stores.tips,
|
Stores.tips,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
|
@ -42,7 +42,7 @@ import { codecForRecoupConfirmation } from "../types/talerTypes";
|
|||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { forceQueryReserve } from "./reserves";
|
import { forceQueryReserve } from "./reserves";
|
||||||
|
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
import { RefreshReason, OperationError } from "../types/walletTypes";
|
import { RefreshReason, OperationError } from "../types/walletTypes";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
@ -266,7 +266,7 @@ async function recoupRefreshCoin(
|
|||||||
).amount;
|
).amount;
|
||||||
console.log(
|
console.log(
|
||||||
"recoup: setting old coin amount to",
|
"recoup: setting old coin amount to",
|
||||||
Amounts.toString(oldCoin.currentAmount),
|
Amounts.stringify(oldCoin.currentAmount),
|
||||||
);
|
);
|
||||||
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
|
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
|
||||||
await tx.put(Stores.coins, revokedCoin);
|
await tx.put(Stores.coins, revokedCoin);
|
||||||
|
@ -14,8 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AmountJson } from "../util/amounts";
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
import * as Amounts from "../util/amounts";
|
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
Stores,
|
Stores,
|
||||||
@ -239,7 +238,7 @@ async function refreshMelt(
|
|||||||
denom_pub_hash: coin.denomPubHash,
|
denom_pub_hash: coin.denomPubHash,
|
||||||
denom_sig: coin.denomSig,
|
denom_sig: coin.denomSig,
|
||||||
rc: refreshSession.hash,
|
rc: refreshSession.hash,
|
||||||
value_with_fee: Amounts.toString(refreshSession.amountRefreshInput),
|
value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
|
||||||
};
|
};
|
||||||
logger.trace(`melt request for coin:`, meltReq);
|
logger.trace(`melt request for coin:`, meltReq);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, meltReq);
|
const resp = await ws.http.postJson(reqUrl.href, meltReq);
|
||||||
|
@ -41,7 +41,7 @@ import {
|
|||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { parseRefundUri } from "../util/taleruri";
|
import { parseRefundUri } from "../util/taleruri";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
MerchantRefundPermission,
|
MerchantRefundPermission,
|
||||||
MerchantRefundResponse,
|
MerchantRefundResponse,
|
||||||
@ -476,7 +476,7 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
`commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`,
|
`commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`,
|
||||||
);
|
);
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`coin amount before is ${Amounts.toString(c.currentAmount)}`,
|
`coin amount before is ${Amounts.stringify(c.currentAmount)}`,
|
||||||
);
|
);
|
||||||
logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`);
|
logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`);
|
||||||
logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`);
|
logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`);
|
||||||
@ -486,7 +486,7 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
||||||
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`coin amount after is ${Amounts.toString(c.currentAmount)}`,
|
`coin amount after is ${Amounts.stringify(c.currentAmount)}`,
|
||||||
);
|
);
|
||||||
await tx.put(Stores.coins, c);
|
await tx.put(Stores.coins, c);
|
||||||
};
|
};
|
||||||
|
@ -28,14 +28,17 @@ import {
|
|||||||
ReserveRecord,
|
ReserveRecord,
|
||||||
CurrencyRecord,
|
CurrencyRecord,
|
||||||
Stores,
|
Stores,
|
||||||
WithdrawalSessionRecord,
|
WithdrawalGroupRecord,
|
||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
ReserveUpdatedEventRecord,
|
ReserveUpdatedEventRecord,
|
||||||
|
WalletReserveHistoryItemType,
|
||||||
|
DenominationRecord,
|
||||||
|
PlanchetRecord,
|
||||||
|
WithdrawalSourceType,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { TransactionAbort } from "../util/query";
|
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
updateExchangeFromUrl,
|
updateExchangeFromUrl,
|
||||||
getExchangeTrust,
|
getExchangeTrust,
|
||||||
@ -50,7 +53,7 @@ import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
|||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import {
|
import {
|
||||||
getVerifiedWithdrawDenomList,
|
getVerifiedWithdrawDenomList,
|
||||||
processWithdrawSession,
|
processWithdrawGroup,
|
||||||
getBankWithdrawalInfo,
|
getBankWithdrawalInfo,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import {
|
import {
|
||||||
@ -61,6 +64,10 @@ import {
|
|||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { codecForReserveStatus } from "../types/ReserveStatus";
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
|
import {
|
||||||
|
reconcileReserveHistory,
|
||||||
|
summarizeReserveHistory,
|
||||||
|
} from "../util/reserveHistoryUtil";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -98,11 +105,7 @@ export async function createReserve(
|
|||||||
|
|
||||||
const reserveRecord: ReserveRecord = {
|
const reserveRecord: ReserveRecord = {
|
||||||
timestampCreated: now,
|
timestampCreated: now,
|
||||||
amountWithdrawAllocated: Amounts.getZero(currency),
|
|
||||||
amountWithdrawCompleted: Amounts.getZero(currency),
|
|
||||||
amountWithdrawRemaining: Amounts.getZero(currency),
|
|
||||||
exchangeBaseUrl: canonExchange,
|
exchangeBaseUrl: canonExchange,
|
||||||
amountInitiallyRequested: req.amount,
|
|
||||||
reservePriv: keypair.priv,
|
reservePriv: keypair.priv,
|
||||||
reservePub: keypair.pub,
|
reservePub: keypair.pub,
|
||||||
senderWire: req.senderWire,
|
senderWire: req.senderWire,
|
||||||
@ -115,8 +118,14 @@ export async function createReserve(
|
|||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
reserveTransactions: [],
|
reserveTransactions: [],
|
||||||
|
currency: req.amount.currency,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reserveRecord.reserveTransactions.push({
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: req.amount,
|
||||||
|
});
|
||||||
|
|
||||||
const senderWire = req.senderWire;
|
const senderWire = req.senderWire;
|
||||||
if (senderWire) {
|
if (senderWire) {
|
||||||
const rec = {
|
const rec = {
|
||||||
@ -460,6 +469,7 @@ async function updateReserve(
|
|||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
const reserveInfo = codecForReserveStatus().decode(respJson);
|
const reserveInfo = codecForReserveStatus().decode(respJson);
|
||||||
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
||||||
|
const currency = balance.currency;
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.reserves, Stores.reserveUpdatedEvents],
|
[Stores.reserves, Stores.reserveUpdatedEvents],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
@ -477,60 +487,41 @@ async function updateReserve(
|
|||||||
|
|
||||||
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
// FIXME: check / compare history!
|
const reconciled = reconcileReserveHistory(
|
||||||
if (!r.lastSuccessfulStatusQuery) {
|
r.reserveTransactions,
|
||||||
// FIXME: check if this matches initial expectations
|
reserveInfo.history,
|
||||||
r.amountWithdrawRemaining = balance;
|
);
|
||||||
|
|
||||||
|
console.log("reconciled history:", JSON.stringify(reconciled, undefined, 2));
|
||||||
|
|
||||||
|
const summary = summarizeReserveHistory(
|
||||||
|
reconciled.updatedLocalHistory,
|
||||||
|
currency,
|
||||||
|
);
|
||||||
|
console.log("summary", summary);
|
||||||
|
|
||||||
|
if (
|
||||||
|
reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
|
||||||
|
0
|
||||||
|
) {
|
||||||
const reserveUpdate: ReserveUpdatedEventRecord = {
|
const reserveUpdate: ReserveUpdatedEventRecord = {
|
||||||
reservePub: r.reservePub,
|
reservePub: r.reservePub,
|
||||||
timestamp: getTimestampNow(),
|
timestamp: getTimestampNow(),
|
||||||
amountReserveBalance: Amounts.toString(balance),
|
amountReserveBalance: Amounts.stringify(balance),
|
||||||
amountExpected: Amounts.toString(reserve.amountInitiallyRequested),
|
amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
|
||||||
newHistoryTransactions,
|
newHistoryTransactions,
|
||||||
reserveUpdateId,
|
reserveUpdateId,
|
||||||
};
|
};
|
||||||
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
||||||
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
||||||
|
r.retryInfo = initRetryInfo();
|
||||||
} else {
|
} else {
|
||||||
const expectedBalance = Amounts.add(
|
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
r.amountWithdrawRemaining,
|
r.retryInfo = initRetryInfo(false);
|
||||||
Amounts.sub(r.amountWithdrawAllocated, r.amountWithdrawCompleted)
|
|
||||||
.amount,
|
|
||||||
);
|
|
||||||
const cmp = Amounts.cmp(balance, expectedBalance.amount);
|
|
||||||
if (cmp == 0) {
|
|
||||||
// Nothing changed, go back to sleep!
|
|
||||||
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
|
||||||
} else if (cmp > 0) {
|
|
||||||
const extra = Amounts.sub(balance, expectedBalance.amount).amount;
|
|
||||||
r.amountWithdrawRemaining = Amounts.add(
|
|
||||||
r.amountWithdrawRemaining,
|
|
||||||
extra,
|
|
||||||
).amount;
|
|
||||||
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
|
||||||
} else {
|
|
||||||
// We're missing some money.
|
|
||||||
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
|
||||||
}
|
|
||||||
if (r.reserveStatus !== ReserveRecordStatus.DORMANT) {
|
|
||||||
const reserveUpdate: ReserveUpdatedEventRecord = {
|
|
||||||
reservePub: r.reservePub,
|
|
||||||
timestamp: getTimestampNow(),
|
|
||||||
amountReserveBalance: Amounts.toString(balance),
|
|
||||||
amountExpected: Amounts.toString(expectedBalance.amount),
|
|
||||||
newHistoryTransactions,
|
|
||||||
reserveUpdateId,
|
|
||||||
};
|
|
||||||
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
r.lastSuccessfulStatusQuery = getTimestampNow();
|
r.lastSuccessfulStatusQuery = getTimestampNow();
|
||||||
if (r.reserveStatus == ReserveRecordStatus.DORMANT) {
|
r.reserveTransactions = reconciled.updatedLocalHistory;
|
||||||
r.retryInfo = initRetryInfo(false);
|
r.lastError = undefined;
|
||||||
} else {
|
|
||||||
r.retryInfo = initRetryInfo();
|
|
||||||
}
|
|
||||||
r.reserveTransactions = reserveInfo.history;
|
|
||||||
await tx.put(Stores.reserves, r);
|
await tx.put(Stores.reserves, r);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -607,6 +598,33 @@ export async function confirmReserve(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makePlanchet(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
reserve: ReserveRecord,
|
||||||
|
denom: DenominationRecord,
|
||||||
|
): Promise<PlanchetRecord> {
|
||||||
|
const r = await ws.cryptoApi.createPlanchet({
|
||||||
|
denomPub: denom.denomPub,
|
||||||
|
feeWithdraw: denom.feeWithdraw,
|
||||||
|
reservePriv: reserve.reservePriv,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
value: denom.value,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
blindingKey: r.blindingKey,
|
||||||
|
coinEv: r.coinEv,
|
||||||
|
coinPriv: r.coinPriv,
|
||||||
|
coinPub: r.coinPub,
|
||||||
|
coinValue: r.coinValue,
|
||||||
|
denomPub: r.denomPub,
|
||||||
|
denomPubHash: r.denomPubHash,
|
||||||
|
isFromTip: false,
|
||||||
|
reservePub: r.reservePub,
|
||||||
|
withdrawSig: r.withdrawSig,
|
||||||
|
coinEvHash: r.coinEvHash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Withdraw coins from a reserve until it is empty.
|
* Withdraw coins from a reserve until it is empty.
|
||||||
*
|
*
|
||||||
@ -626,7 +644,12 @@ async function depleteReserve(
|
|||||||
}
|
}
|
||||||
logger.trace(`depleting reserve ${reservePub}`);
|
logger.trace(`depleting reserve ${reservePub}`);
|
||||||
|
|
||||||
const withdrawAmount = reserve.amountWithdrawRemaining;
|
const summary = summarizeReserveHistory(
|
||||||
|
reserve.reserveTransactions,
|
||||||
|
reserve.currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withdrawAmount = summary.unclaimedReserveAmount;
|
||||||
|
|
||||||
logger.trace(`getting denom list`);
|
logger.trace(`getting denom list`);
|
||||||
|
|
||||||
@ -637,36 +660,47 @@ async function depleteReserve(
|
|||||||
);
|
);
|
||||||
logger.trace(`got denom list`);
|
logger.trace(`got denom list`);
|
||||||
if (denomsForWithdraw.length === 0) {
|
if (denomsForWithdraw.length === 0) {
|
||||||
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
|
// Only complain about inability to withdraw if we
|
||||||
const opErr = {
|
// didn't withdraw before.
|
||||||
type: "internal",
|
if (Amounts.isZero(summary.withdrawnAmount)) {
|
||||||
message: m,
|
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
|
||||||
details: {},
|
const opErr = {
|
||||||
};
|
type: "internal",
|
||||||
await incrementReserveRetry(ws, reserve.reservePub, opErr);
|
message: m,
|
||||||
console.log(m);
|
details: {},
|
||||||
throw new OperationFailedAndReportedError(opErr);
|
};
|
||||||
|
await incrementReserveRetry(ws, reserve.reservePub, opErr);
|
||||||
|
console.log(m);
|
||||||
|
throw new OperationFailedAndReportedError(opErr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("selected denominations");
|
logger.trace("selected denominations");
|
||||||
|
|
||||||
const withdrawalSessionId = encodeCrock(randomBytes(32));
|
const withdrawalGroupId = encodeCrock(randomBytes(32));
|
||||||
|
|
||||||
const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
|
const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
|
||||||
.amount;
|
.amount;
|
||||||
|
|
||||||
const withdrawalRecord: WithdrawalSessionRecord = {
|
const planchets: PlanchetRecord[] = [];
|
||||||
withdrawSessionId: withdrawalSessionId,
|
for (const d of denomsForWithdraw) {
|
||||||
|
const p = await makePlanchet(ws, reserve, d);
|
||||||
|
planchets.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withdrawalRecord: WithdrawalGroupRecord = {
|
||||||
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
||||||
source: {
|
source: {
|
||||||
type: "reserve",
|
type: WithdrawalSourceType.Reserve,
|
||||||
reservePub: reserve.reservePub,
|
reservePub: reserve.reservePub,
|
||||||
},
|
},
|
||||||
rawWithdrawalAmount: withdrawAmount,
|
rawWithdrawalAmount: withdrawAmount,
|
||||||
timestampStart: getTimestampNow(),
|
timestampStart: getTimestampNow(),
|
||||||
denoms: denomsForWithdraw.map((x) => x.denomPub),
|
denoms: denomsForWithdraw.map((x) => x.denomPub),
|
||||||
withdrawn: denomsForWithdraw.map((x) => false),
|
withdrawn: denomsForWithdraw.map((x) => false),
|
||||||
planchets: denomsForWithdraw.map((x) => undefined),
|
planchets,
|
||||||
totalCoinValue,
|
totalCoinValue,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastErrorPerCoin: {},
|
lastErrorPerCoin: {},
|
||||||
@ -679,53 +713,54 @@ async function depleteReserve(
|
|||||||
const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
|
const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
|
||||||
.amount;
|
.amount;
|
||||||
|
|
||||||
function mutateReserve(r: ReserveRecord): ReserveRecord {
|
|
||||||
const remaining = Amounts.sub(
|
|
||||||
r.amountWithdrawRemaining,
|
|
||||||
totalWithdrawAmount,
|
|
||||||
);
|
|
||||||
if (remaining.saturated) {
|
|
||||||
console.error("can't create planchets, saturated");
|
|
||||||
throw TransactionAbort;
|
|
||||||
}
|
|
||||||
const allocated = Amounts.add(
|
|
||||||
r.amountWithdrawAllocated,
|
|
||||||
totalWithdrawAmount,
|
|
||||||
);
|
|
||||||
if (allocated.saturated) {
|
|
||||||
console.error("can't create planchets, saturated");
|
|
||||||
throw TransactionAbort;
|
|
||||||
}
|
|
||||||
r.amountWithdrawRemaining = remaining.amount;
|
|
||||||
r.amountWithdrawAllocated = allocated.amount;
|
|
||||||
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
|
||||||
r.retryInfo = initRetryInfo(false);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await ws.db.runWithWriteTransaction(
|
const success = await ws.db.runWithWriteTransaction(
|
||||||
[Stores.withdrawalSession, Stores.reserves],
|
[Stores.withdrawalGroups, Stores.reserves],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const myReserve = await tx.get(Stores.reserves, reservePub);
|
const newReserve = await tx.get(Stores.reserves, reservePub);
|
||||||
if (!myReserve) {
|
if (!newReserve) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
|
const newSummary = summarizeReserveHistory(
|
||||||
await tx.put(Stores.withdrawalSession, withdrawalRecord);
|
newReserve.reserveTransactions,
|
||||||
|
newReserve.currency,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0
|
||||||
|
) {
|
||||||
|
// Something must have happened concurrently!
|
||||||
|
logger.error(
|
||||||
|
"aborting withdrawal session, likely concurrent withdrawal happened",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < planchets.length; i++) {
|
||||||
|
const amt = Amounts.add(
|
||||||
|
denomsForWithdraw[i].value,
|
||||||
|
denomsForWithdraw[i].feeWithdraw,
|
||||||
|
).amount;
|
||||||
|
newReserve.reserveTransactions.push({
|
||||||
|
type: WalletReserveHistoryItemType.Withdraw,
|
||||||
|
expectedAmount: amt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
|
newReserve.retryInfo = initRetryInfo(false);
|
||||||
|
await tx.put(Stores.reserves, newReserve);
|
||||||
|
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log("processing new withdraw session");
|
console.log("processing new withdraw group");
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.WithdrawSessionCreated,
|
type: NotificationType.WithdrawGroupCreated,
|
||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
});
|
});
|
||||||
await processWithdrawSession(ws, withdrawalSessionId);
|
await processWithdrawGroup(ws, withdrawalGroupId);
|
||||||
} else {
|
} else {
|
||||||
console.trace("withdraw session already existed");
|
console.trace("withdraw session already existed");
|
||||||
}
|
}
|
||||||
|
@ -28,14 +28,15 @@ import * as Amounts from "../util/amounts";
|
|||||||
import {
|
import {
|
||||||
Stores,
|
Stores,
|
||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
WithdrawalSessionRecord,
|
WithdrawalGroupRecord,
|
||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
|
WithdrawalSourceType,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
getVerifiedWithdrawDenomList,
|
getVerifiedWithdrawDenomList,
|
||||||
processWithdrawSession,
|
processWithdrawGroup,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
@ -246,8 +247,10 @@ async function processTipImpl(
|
|||||||
|
|
||||||
const planchets: PlanchetRecord[] = [];
|
const planchets: PlanchetRecord[] = [];
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < tipRecord.planchets.length; i++) {
|
for (let i = 0; i < tipRecord.planchets.length; i++) {
|
||||||
const tipPlanchet = tipRecord.planchets[i];
|
const tipPlanchet = tipRecord.planchets[i];
|
||||||
|
const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
|
||||||
const planchet: PlanchetRecord = {
|
const planchet: PlanchetRecord = {
|
||||||
blindingKey: tipPlanchet.blindingKey,
|
blindingKey: tipPlanchet.blindingKey,
|
||||||
coinEv: tipPlanchet.coinEv,
|
coinEv: tipPlanchet.coinEv,
|
||||||
@ -259,22 +262,23 @@ async function processTipImpl(
|
|||||||
reservePub: response.reserve_pub,
|
reservePub: response.reserve_pub,
|
||||||
withdrawSig: response.reserve_sigs[i].reserve_sig,
|
withdrawSig: response.reserve_sigs[i].reserve_sig,
|
||||||
isFromTip: true,
|
isFromTip: true,
|
||||||
|
coinEvHash,
|
||||||
};
|
};
|
||||||
planchets.push(planchet);
|
planchets.push(planchet);
|
||||||
}
|
}
|
||||||
|
|
||||||
const withdrawalSessionId = encodeCrock(getRandomBytes(32));
|
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
const withdrawalSession: WithdrawalSessionRecord = {
|
const withdrawalGroup: WithdrawalGroupRecord = {
|
||||||
denoms: planchets.map((x) => x.denomPub),
|
denoms: planchets.map((x) => x.denomPub),
|
||||||
exchangeBaseUrl: tipRecord.exchangeUrl,
|
exchangeBaseUrl: tipRecord.exchangeUrl,
|
||||||
planchets: planchets,
|
planchets: planchets,
|
||||||
source: {
|
source: {
|
||||||
type: "tip",
|
type: WithdrawalSourceType.Tip,
|
||||||
tipId: tipRecord.tipId,
|
tipId: tipRecord.tipId,
|
||||||
},
|
},
|
||||||
timestampStart: getTimestampNow(),
|
timestampStart: getTimestampNow(),
|
||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
rawWithdrawalAmount: tipRecord.amount,
|
rawWithdrawalAmount: tipRecord.amount,
|
||||||
withdrawn: planchets.map((x) => false),
|
withdrawn: planchets.map((x) => false),
|
||||||
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
||||||
@ -285,7 +289,7 @@ async function processTipImpl(
|
|||||||
};
|
};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.tips, Stores.withdrawalSession],
|
[Stores.tips, Stores.withdrawalGroups],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const tr = await tx.get(Stores.tips, tipId);
|
const tr = await tx.get(Stores.tips, tipId);
|
||||||
if (!tr) {
|
if (!tr) {
|
||||||
@ -298,11 +302,11 @@ async function processTipImpl(
|
|||||||
tr.retryInfo = initRetryInfo(false);
|
tr.retryInfo = initRetryInfo(false);
|
||||||
|
|
||||||
await tx.put(Stores.tips, tr);
|
await tx.put(Stores.tips, tr);
|
||||||
await tx.put(Stores.withdrawalSession, withdrawalSession);
|
await tx.put(Stores.withdrawalGroups, withdrawalGroup);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await processWithdrawSession(ws, withdrawalSessionId);
|
await processWithdrawGroup(ws, withdrawalGroupId);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ import {
|
|||||||
timestampCmp,
|
timestampCmp,
|
||||||
timestampSubtractDuraction,
|
timestampSubtractDuraction,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
|
import { summarizeReserveHistory, ReserveHistorySummary } from "../util/reserveHistoryUtil";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -158,29 +159,29 @@ async function getPossibleDenoms(
|
|||||||
*/
|
*/
|
||||||
async function processPlanchet(
|
async function processPlanchet(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalGroupId: string,
|
||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const withdrawalSession = await ws.db.get(
|
const withdrawalGroup = await ws.db.get(
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
withdrawalSessionId,
|
withdrawalGroupId,
|
||||||
);
|
);
|
||||||
if (!withdrawalSession) {
|
if (!withdrawalGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (withdrawalSession.withdrawn[coinIdx]) {
|
if (withdrawalGroup.withdrawn[coinIdx]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (withdrawalSession.source.type === "reserve") {
|
if (withdrawalGroup.source.type === "reserve") {
|
||||||
}
|
}
|
||||||
const planchet = withdrawalSession.planchets[coinIdx];
|
const planchet = withdrawalGroup.planchets[coinIdx];
|
||||||
if (!planchet) {
|
if (!planchet) {
|
||||||
console.log("processPlanchet: planchet not found");
|
console.log("processPlanchet: planchet not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const exchange = await ws.db.get(
|
const exchange = await ws.db.get(
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
withdrawalSession.exchangeBaseUrl,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
console.error("db inconsistent: exchange for planchet not found");
|
console.error("db inconsistent: exchange for planchet not found");
|
||||||
@ -188,7 +189,7 @@ async function processPlanchet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
const denom = await ws.db.get(Stores.denominations, [
|
||||||
withdrawalSession.exchangeBaseUrl,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
planchet.denomPub,
|
planchet.denomPub,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -232,24 +233,24 @@ async function processPlanchet(
|
|||||||
denomPub: planchet.denomPub,
|
denomPub: planchet.denomPub,
|
||||||
denomPubHash: planchet.denomPubHash,
|
denomPubHash: planchet.denomPubHash,
|
||||||
denomSig,
|
denomSig,
|
||||||
exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
|
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
coinSource: {
|
coinSource: {
|
||||||
type: CoinSourceType.Withdraw,
|
type: CoinSourceType.Withdraw,
|
||||||
coinIndex: coinIdx,
|
coinIndex: coinIdx,
|
||||||
reservePub: planchet.reservePub,
|
reservePub: planchet.reservePub,
|
||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
},
|
},
|
||||||
suspended: false,
|
suspended: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let withdrawSessionFinished = false;
|
let withdrawalGroupFinished = false;
|
||||||
let reserveDepleted = false;
|
let summary: ReserveHistorySummary | undefined = undefined;
|
||||||
|
|
||||||
const success = await ws.db.runWithWriteTransaction(
|
const success = await ws.db.runWithWriteTransaction(
|
||||||
[Stores.coins, Stores.withdrawalSession, Stores.reserves],
|
[Stores.coins, Stores.withdrawalGroups, Stores.reserves],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
|
const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -269,23 +270,13 @@ async function processPlanchet(
|
|||||||
ws.timestampFinish = getTimestampNow();
|
ws.timestampFinish = getTimestampNow();
|
||||||
ws.lastError = undefined;
|
ws.lastError = undefined;
|
||||||
ws.retryInfo = initRetryInfo(false);
|
ws.retryInfo = initRetryInfo(false);
|
||||||
withdrawSessionFinished = true;
|
withdrawalGroupFinished = true;
|
||||||
}
|
}
|
||||||
await tx.put(Stores.withdrawalSession, ws);
|
await tx.put(Stores.withdrawalGroups, ws);
|
||||||
if (!planchet.isFromTip) {
|
if (!planchet.isFromTip) {
|
||||||
const r = await tx.get(Stores.reserves, planchet.reservePub);
|
const r = await tx.get(Stores.reserves, planchet.reservePub);
|
||||||
if (r) {
|
if (r) {
|
||||||
r.amountWithdrawCompleted = Amounts.add(
|
summary = summarizeReserveHistory(r.reserveTransactions, r.currency);
|
||||||
r.amountWithdrawCompleted,
|
|
||||||
Amounts.add(denom.value, denom.feeWithdraw).amount,
|
|
||||||
).amount;
|
|
||||||
if (
|
|
||||||
Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) ==
|
|
||||||
0
|
|
||||||
) {
|
|
||||||
reserveDepleted = true;
|
|
||||||
}
|
|
||||||
await tx.put(Stores.reserves, r);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tx.add(Stores.coins, coin);
|
await tx.add(Stores.coins, coin);
|
||||||
@ -299,17 +290,10 @@ async function processPlanchet(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withdrawSessionFinished) {
|
if (withdrawalGroupFinished) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.WithdrawSessionFinished,
|
type: NotificationType.WithdrawGroupFinished,
|
||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawalSource: withdrawalGroup.source,
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reserveDepleted && withdrawalSession.source.type === "reserve") {
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.ReserveDepleted,
|
|
||||||
reservePub: withdrawalSession.source.reservePub,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -383,113 +367,15 @@ export async function getVerifiedWithdrawDenomList(
|
|||||||
return selectedDenoms;
|
return selectedDenoms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makePlanchet(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
withdrawalSessionId: string,
|
|
||||||
coinIndex: number,
|
|
||||||
): Promise<void> {
|
|
||||||
const withdrawalSession = await ws.db.get(
|
|
||||||
Stores.withdrawalSession,
|
|
||||||
withdrawalSessionId,
|
|
||||||
);
|
|
||||||
if (!withdrawalSession) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const src = withdrawalSession.source;
|
|
||||||
if (src.type !== "reserve") {
|
|
||||||
throw Error("invalid state");
|
|
||||||
}
|
|
||||||
const reserve = await ws.db.get(Stores.reserves, src.reservePub);
|
|
||||||
if (!reserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
|
||||||
withdrawalSession.exchangeBaseUrl,
|
|
||||||
withdrawalSession.denoms[coinIndex],
|
|
||||||
]);
|
|
||||||
if (!denom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const r = await ws.cryptoApi.createPlanchet({
|
|
||||||
denomPub: denom.denomPub,
|
|
||||||
feeWithdraw: denom.feeWithdraw,
|
|
||||||
reservePriv: reserve.reservePriv,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
value: denom.value,
|
|
||||||
});
|
|
||||||
const newPlanchet: PlanchetRecord = {
|
|
||||||
blindingKey: r.blindingKey,
|
|
||||||
coinEv: r.coinEv,
|
|
||||||
coinPriv: r.coinPriv,
|
|
||||||
coinPub: r.coinPub,
|
|
||||||
coinValue: r.coinValue,
|
|
||||||
denomPub: r.denomPub,
|
|
||||||
denomPubHash: r.denomPubHash,
|
|
||||||
isFromTip: false,
|
|
||||||
reservePub: r.reservePub,
|
|
||||||
withdrawSig: r.withdrawSig,
|
|
||||||
};
|
|
||||||
await ws.db.runWithWriteTransaction(
|
|
||||||
[Stores.withdrawalSession],
|
|
||||||
async (tx) => {
|
|
||||||
const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
|
|
||||||
if (!myWs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (myWs.planchets[coinIndex]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWs.planchets[coinIndex] = newPlanchet;
|
|
||||||
await tx.put(Stores.withdrawalSession, myWs);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processWithdrawCoin(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
withdrawalSessionId: string,
|
|
||||||
coinIndex: number,
|
|
||||||
) {
|
|
||||||
logger.trace("starting withdraw for coin", coinIndex);
|
|
||||||
const withdrawalSession = await ws.db.get(
|
|
||||||
Stores.withdrawalSession,
|
|
||||||
withdrawalSessionId,
|
|
||||||
);
|
|
||||||
if (!withdrawalSession) {
|
|
||||||
console.log("ws doesn't exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planchet = withdrawalSession.planchets[coinIndex];
|
|
||||||
|
|
||||||
if (planchet) {
|
|
||||||
const coin = await ws.db.get(Stores.coins, planchet.coinPub);
|
|
||||||
|
|
||||||
if (coin) {
|
|
||||||
console.log("coin already exists");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!withdrawalSession.planchets[coinIndex]) {
|
|
||||||
const key = `${withdrawalSessionId}-${coinIndex}`;
|
|
||||||
await ws.memoMakePlanchet.memo(key, async () => {
|
|
||||||
logger.trace("creating planchet for coin", coinIndex);
|
|
||||||
return makePlanchet(ws, withdrawalSessionId, coinIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await processPlanchet(ws, withdrawalSessionId, coinIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function incrementWithdrawalRetry(
|
async function incrementWithdrawalRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalGroupId: string,
|
||||||
err: OperationError | undefined,
|
err: OperationError | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.withdrawalSession],
|
[Stores.withdrawalGroups],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
|
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
||||||
if (!wsr) {
|
if (!wsr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -499,30 +385,30 @@ async function incrementWithdrawalRetry(
|
|||||||
wsr.retryInfo.retryCounter++;
|
wsr.retryInfo.retryCounter++;
|
||||||
updateRetryInfoTimeout(wsr.retryInfo);
|
updateRetryInfoTimeout(wsr.retryInfo);
|
||||||
wsr.lastError = err;
|
wsr.lastError = err;
|
||||||
await tx.put(Stores.withdrawalSession, wsr);
|
await tx.put(Stores.withdrawalGroups, wsr);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.notify({ type: NotificationType.WithdrawOperationError });
|
ws.notify({ type: NotificationType.WithdrawOperationError });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processWithdrawSession(
|
export async function processWithdrawGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalGroupId: string,
|
||||||
forceNow: boolean = false,
|
forceNow: boolean = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onOpErr = (e: OperationError) =>
|
const onOpErr = (e: OperationError) =>
|
||||||
incrementWithdrawalRetry(ws, withdrawalSessionId, e);
|
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
|
||||||
await guardOperationException(
|
await guardOperationException(
|
||||||
() => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow),
|
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
|
||||||
onOpErr,
|
onOpErr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetWithdrawSessionRetry(
|
async function resetWithdrawalGroupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalGroupId: string,
|
||||||
) {
|
) {
|
||||||
await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
|
await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
|
||||||
if (x.retryInfo.active) {
|
if (x.retryInfo.active) {
|
||||||
x.retryInfo = initRetryInfo();
|
x.retryInfo = initRetryInfo();
|
||||||
}
|
}
|
||||||
@ -530,26 +416,26 @@ async function resetWithdrawSessionRetry(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processWithdrawSessionImpl(
|
async function processWithdrawGroupImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalGroupId: string,
|
||||||
forceNow: boolean,
|
forceNow: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.trace("processing withdraw session", withdrawalSessionId);
|
logger.trace("processing withdraw group", withdrawalGroupId);
|
||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetWithdrawSessionRetry(ws, withdrawalSessionId);
|
await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
|
||||||
}
|
}
|
||||||
const withdrawalSession = await ws.db.get(
|
const withdrawalGroup = await ws.db.get(
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
withdrawalSessionId,
|
withdrawalGroupId,
|
||||||
);
|
);
|
||||||
if (!withdrawalSession) {
|
if (!withdrawalGroup) {
|
||||||
logger.trace("withdraw session doesn't exist");
|
logger.trace("withdraw session doesn't exist");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ps = withdrawalSession.denoms.map((d, i) =>
|
const ps = withdrawalGroup.denoms.map((d, i) =>
|
||||||
processWithdrawCoin(ws, withdrawalSessionId, i),
|
processPlanchet(ws, withdrawalGroupId, i),
|
||||||
);
|
);
|
||||||
await Promise.all(ps);
|
await Promise.all(ps);
|
||||||
return;
|
return;
|
||||||
|
@ -151,7 +151,7 @@ export interface WalletReserveHistoryCreditItem {
|
|||||||
/**
|
/**
|
||||||
* Amount we expect to see credited.
|
* Amount we expect to see credited.
|
||||||
*/
|
*/
|
||||||
expectedAmount?: string;
|
expectedAmount?: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item from the reserve transaction history that this
|
* Item from the reserve transaction history that this
|
||||||
@ -161,7 +161,15 @@ export interface WalletReserveHistoryCreditItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WalletReserveHistoryWithdrawItem {
|
export interface WalletReserveHistoryWithdrawItem {
|
||||||
expectedAmount?: string;
|
expectedAmount?: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the blinded coin.
|
||||||
|
*
|
||||||
|
* When this value is set, it indicates that a withdrawal is active
|
||||||
|
* in the wallet for the
|
||||||
|
*/
|
||||||
|
expectedCoinEvHash?: string;
|
||||||
|
|
||||||
type: WalletReserveHistoryItemType.Withdraw;
|
type: WalletReserveHistoryItemType.Withdraw;
|
||||||
|
|
||||||
@ -188,7 +196,7 @@ export interface WalletReserveHistoryRecoupItem {
|
|||||||
/**
|
/**
|
||||||
* Amount we expect to see recouped.
|
* Amount we expect to see recouped.
|
||||||
*/
|
*/
|
||||||
expectedAmount?: string;
|
expectedAmount?: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item from the reserve transaction history that this
|
* Item from the reserve transaction history that this
|
||||||
@ -222,6 +230,11 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currency of the reserve.
|
||||||
|
*/
|
||||||
|
currency: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the reserve was created.
|
* Time when the reserve was created.
|
||||||
*/
|
*/
|
||||||
@ -237,34 +250,13 @@ export interface ReserveRecord {
|
|||||||
timestampReserveInfoPosted: Timestamp | undefined;
|
timestampReserveInfoPosted: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the reserve was confirmed.
|
* Time when the reserve was confirmed, either manually by the user
|
||||||
|
* or by the bank.
|
||||||
*
|
*
|
||||||
* Set to 0 if not confirmed yet.
|
* Set to undefined if not confirmed yet.
|
||||||
*/
|
*/
|
||||||
timestampConfirmed: Timestamp | undefined;
|
timestampConfirmed: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that's still available for withdrawing
|
|
||||||
* from this reserve.
|
|
||||||
*/
|
|
||||||
amountWithdrawRemaining: AmountJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount allocated for withdrawing.
|
|
||||||
* The corresponding withdraw operation may or may not
|
|
||||||
* have been completed yet.
|
|
||||||
*/
|
|
||||||
amountWithdrawAllocated: AmountJson;
|
|
||||||
|
|
||||||
amountWithdrawCompleted: AmountJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount requested when the reserve was created.
|
|
||||||
* When a reserve is re-used (rare!) the current_amount can
|
|
||||||
* be higher than the requested_amount
|
|
||||||
*/
|
|
||||||
amountInitiallyRequested: AmountJson;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire information (as payto URI) for the bank account that
|
* Wire information (as payto URI) for the bank account that
|
||||||
* transfered funds for this reserve.
|
* transfered funds for this reserve.
|
||||||
@ -305,7 +297,7 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
|
|
||||||
reserveTransactions: ReserveTransaction[];
|
reserveTransactions: WalletReserveHistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -627,6 +619,7 @@ export interface PlanchetRecord {
|
|||||||
blindingKey: string;
|
blindingKey: string;
|
||||||
withdrawSig: string;
|
withdrawSig: string;
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
|
coinEvHash: string;
|
||||||
coinValue: AmountJson;
|
coinValue: AmountJson;
|
||||||
isFromTip: boolean;
|
isFromTip: boolean;
|
||||||
}
|
}
|
||||||
@ -675,7 +668,7 @@ export const enum CoinSourceType {
|
|||||||
|
|
||||||
export interface WithdrawCoinSource {
|
export interface WithdrawCoinSource {
|
||||||
type: CoinSourceType.Withdraw;
|
type: CoinSourceType.Withdraw;
|
||||||
withdrawSessionId: string;
|
withdrawalGroupId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index of the coin in the withdrawal session.
|
* Index of the coin in the withdrawal session.
|
||||||
@ -1362,20 +1355,25 @@ export interface CoinsReturnRecord {
|
|||||||
wire: any;
|
wire: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum WithdrawalSourceType {
|
||||||
|
Tip = "tip",
|
||||||
|
Reserve = "reserve",
|
||||||
|
}
|
||||||
|
|
||||||
export interface WithdrawalSourceTip {
|
export interface WithdrawalSourceTip {
|
||||||
type: "tip";
|
type: WithdrawalSourceType.Tip;
|
||||||
tipId: string;
|
tipId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawalSourceReserve {
|
export interface WithdrawalSourceReserve {
|
||||||
type: "reserve";
|
type: WithdrawalSourceType.Reserve;
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
|
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
|
||||||
|
|
||||||
export interface WithdrawalSessionRecord {
|
export interface WithdrawalGroupRecord {
|
||||||
withdrawSessionId: string;
|
withdrawalGroupId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Withdrawal source. Fields that don't apply to the respective
|
* Withdrawal source. Fields that don't apply to the respective
|
||||||
@ -1636,9 +1634,9 @@ export namespace Stores {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
|
class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("withdrawals", { keyPath: "withdrawSessionId" });
|
super("withdrawals", { keyPath: "withdrawalGroupId" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1697,7 +1695,7 @@ export namespace Stores {
|
|||||||
export const purchases = new PurchasesStore();
|
export const purchases = new PurchasesStore();
|
||||||
export const tips = new TipsStore();
|
export const tips = new TipsStore();
|
||||||
export const senderWires = new SenderWiresStore();
|
export const senderWires = new SenderWiresStore();
|
||||||
export const withdrawalSession = new WithdrawalSessionsStore();
|
export const withdrawalGroups = new WithdrawalGroupsStore();
|
||||||
export const bankWithdrawUris = new BankWithdrawUrisStore();
|
export const bankWithdrawUris = new BankWithdrawUrisStore();
|
||||||
export const refundEvents = new RefundEventsStore();
|
export const refundEvents = new RefundEventsStore();
|
||||||
export const payEvents = new PayEventsStore();
|
export const payEvents = new PayEventsStore();
|
||||||
|
@ -119,8 +119,6 @@ export interface HistoryReserveBalanceUpdatedEvent {
|
|||||||
*/
|
*/
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
|
|
||||||
newHistoryTransactions: ReserveTransaction[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condensed information about the reserve.
|
* Condensed information about the reserve.
|
||||||
*/
|
*/
|
||||||
@ -129,13 +127,17 @@ export interface HistoryReserveBalanceUpdatedEvent {
|
|||||||
/**
|
/**
|
||||||
* Amount currently left in the reserve.
|
* Amount currently left in the reserve.
|
||||||
*/
|
*/
|
||||||
amountReserveBalance: string;
|
reserveBalance: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount we expected to be in the reserve at that time,
|
* Amount we still expect to be added to the reserve.
|
||||||
* considering ongoing withdrawals from that reserve.
|
|
||||||
*/
|
*/
|
||||||
amountExpected: string;
|
reserveAwaitedAmount: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that hasn't been withdrawn yet.
|
||||||
|
*/
|
||||||
|
reserveUnclaimedAmount: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -612,7 +614,7 @@ export interface HistoryWithdrawnEvent {
|
|||||||
* Unique identifier for the withdrawal session, can be used to
|
* Unique identifier for the withdrawal session, can be used to
|
||||||
* query more detailed information from the wallet.
|
* query more detailed information from the wallet.
|
||||||
*/
|
*/
|
||||||
withdrawSessionId: string;
|
withdrawalGroupId: string;
|
||||||
|
|
||||||
withdrawalSource: WithdrawalSource;
|
withdrawalSource: WithdrawalSource;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { OperationError } from "./walletTypes";
|
import { OperationError } from "./walletTypes";
|
||||||
|
import { WithdrawCoinSource, WithdrawalSource } from "./dbTypes";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
@ -34,10 +35,9 @@ export const enum NotificationType {
|
|||||||
RefreshUnwarranted = "refresh-unwarranted",
|
RefreshUnwarranted = "refresh-unwarranted",
|
||||||
ReserveUpdated = "reserve-updated",
|
ReserveUpdated = "reserve-updated",
|
||||||
ReserveConfirmed = "reserve-confirmed",
|
ReserveConfirmed = "reserve-confirmed",
|
||||||
ReserveDepleted = "reserve-depleted",
|
|
||||||
ReserveCreated = "reserve-created",
|
ReserveCreated = "reserve-created",
|
||||||
WithdrawSessionCreated = "withdraw-session-created",
|
WithdrawGroupCreated = "withdraw-group-created",
|
||||||
WithdrawSessionFinished = "withdraw-session-finished",
|
WithdrawGroupFinished = "withdraw-group-finished",
|
||||||
WaitingForRetry = "waiting-for-retry",
|
WaitingForRetry = "waiting-for-retry",
|
||||||
RefundStarted = "refund-started",
|
RefundStarted = "refund-started",
|
||||||
RefundQueried = "refund-queried",
|
RefundQueried = "refund-queried",
|
||||||
@ -114,19 +114,14 @@ export interface ReserveConfirmedNotification {
|
|||||||
type: NotificationType.ReserveConfirmed;
|
type: NotificationType.ReserveConfirmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawSessionCreatedNotification {
|
export interface WithdrawalGroupCreatedNotification {
|
||||||
type: NotificationType.WithdrawSessionCreated;
|
type: NotificationType.WithdrawGroupCreated;
|
||||||
withdrawSessionId: string;
|
withdrawalGroupId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawSessionFinishedNotification {
|
export interface WithdrawalGroupFinishedNotification {
|
||||||
type: NotificationType.WithdrawSessionFinished;
|
type: NotificationType.WithdrawGroupFinished;
|
||||||
withdrawSessionId: string;
|
withdrawalSource: WithdrawalSource;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReserveDepletedNotification {
|
|
||||||
type: NotificationType.ReserveDepleted;
|
|
||||||
reservePub: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WaitingForRetryNotification {
|
export interface WaitingForRetryNotification {
|
||||||
@ -210,13 +205,12 @@ export type WalletNotification =
|
|||||||
| ReserveUpdatedNotification
|
| ReserveUpdatedNotification
|
||||||
| ReserveCreatedNotification
|
| ReserveCreatedNotification
|
||||||
| ReserveConfirmedNotification
|
| ReserveConfirmedNotification
|
||||||
| WithdrawSessionFinishedNotification
|
| WithdrawalGroupFinishedNotification
|
||||||
| ReserveDepletedNotification
|
|
||||||
| WaitingForRetryNotification
|
| WaitingForRetryNotification
|
||||||
| RefundStartedNotification
|
| RefundStartedNotification
|
||||||
| RefundFinishedNotification
|
| RefundFinishedNotification
|
||||||
| RefundQueriedNotification
|
| RefundQueriedNotification
|
||||||
| WithdrawSessionCreatedNotification
|
| WithdrawalGroupCreatedNotification
|
||||||
| CoinWithdrawnNotification
|
| CoinWithdrawnNotification
|
||||||
| WildcardNotification
|
| WildcardNotification
|
||||||
| RecoupOperationErrorNotification;
|
| RecoupOperationErrorNotification;
|
||||||
|
@ -214,7 +214,8 @@ export interface PendingRecoupOperation {
|
|||||||
export interface PendingWithdrawOperation {
|
export interface PendingWithdrawOperation {
|
||||||
type: PendingOperationType.Withdraw;
|
type: PendingOperationType.Withdraw;
|
||||||
source: WithdrawalSource;
|
source: WithdrawalSource;
|
||||||
withdrawSessionId: string;
|
lastError: OperationError | undefined;
|
||||||
|
withdrawalGroupId: string;
|
||||||
numCoinsWithdrawn: number;
|
numCoinsWithdrawn: number;
|
||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
import * as Amounts from "../util/amounts";
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
import { ContractTerms, codecForContractTerms } from "./talerTypes";
|
import { codecForContractTerms } from "./talerTypes";
|
||||||
|
|
||||||
const amt = (
|
const amt = (
|
||||||
value: number,
|
value: number,
|
||||||
fraction: number,
|
fraction: number,
|
||||||
currency: string,
|
currency: string,
|
||||||
): Amounts.AmountJson => ({ value, fraction, currency });
|
): AmountJson => ({ value, fraction, currency });
|
||||||
|
|
||||||
test("amount addition (simple)", (t) => {
|
test("amount addition (simple)", (t) => {
|
||||||
const a1 = amt(1, 0, "EUR");
|
const a1 = amt(1, 0, "EUR");
|
||||||
@ -118,13 +118,13 @@ test("amount parsing", (t) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("amount stringification", (t) => {
|
test("amount stringification", (t) => {
|
||||||
t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
|
t.is(Amounts.stringify(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
|
||||||
t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
|
t.is(Amounts.stringify(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
|
||||||
t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
|
t.is(Amounts.stringify(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
|
||||||
t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
|
t.is(Amounts.stringify(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
|
||||||
t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
|
t.is(Amounts.stringify(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
|
||||||
// denormalized
|
// denormalized
|
||||||
t.is(Amounts.toString(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
|
t.is(Amounts.stringify(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
|
||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -427,6 +427,7 @@ export interface PlanchetCreationResult {
|
|||||||
withdrawSig: string;
|
withdrawSig: string;
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
coinValue: AmountJson;
|
coinValue: AmountJson;
|
||||||
|
coinEvHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanchetCreationRequest {
|
export interface PlanchetCreationRequest {
|
||||||
|
@ -299,7 +299,7 @@ export function fromFloat(floatVal: number, currency: string) {
|
|||||||
* Convert to standard human-readable string representation that's
|
* Convert to standard human-readable string representation that's
|
||||||
* also used in JSON formats.
|
* also used in JSON formats.
|
||||||
*/
|
*/
|
||||||
export function toString(a: AmountJson): string {
|
export function stringify(a: AmountJson): string {
|
||||||
const av = a.value + Math.floor(a.fraction / fractionalBase);
|
const av = a.value + Math.floor(a.fraction / fractionalBase);
|
||||||
const af = a.fraction % fractionalBase;
|
const af = a.fraction % fractionalBase;
|
||||||
let s = av.toString();
|
let s = av.toString();
|
||||||
@ -322,7 +322,7 @@ export function toString(a: AmountJson): string {
|
|||||||
/**
|
/**
|
||||||
* Check if the argument is a valid amount in string form.
|
* Check if the argument is a valid amount in string form.
|
||||||
*/
|
*/
|
||||||
export function check(a: any): boolean {
|
function check(a: any): boolean {
|
||||||
if (typeof a !== "string") {
|
if (typeof a !== "string") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -333,3 +333,19 @@ export function check(a: any): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export all amount-related functions here for better IDE experience.
|
||||||
|
export const Amounts = {
|
||||||
|
stringify: stringify,
|
||||||
|
parse: parse,
|
||||||
|
parseOrThrow: parseOrThrow,
|
||||||
|
cmp: cmp,
|
||||||
|
add: add,
|
||||||
|
sum: sum,
|
||||||
|
sub: sub,
|
||||||
|
check: check,
|
||||||
|
getZero: getZero,
|
||||||
|
isZero: isZero,
|
||||||
|
maxAmountValue: maxAmountValue,
|
||||||
|
fromFloat: fromFloat,
|
||||||
|
};
|
286
src/util/reserveHistoryUtil-test.ts
Normal file
286
src/util/reserveHistoryUtil-test.ts
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 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 {
|
||||||
|
reconcileReserveHistory,
|
||||||
|
summarizeReserveHistory,
|
||||||
|
} from "./reserveHistoryUtil";
|
||||||
|
import {
|
||||||
|
WalletReserveHistoryItem,
|
||||||
|
WalletReserveHistoryItemType,
|
||||||
|
} from "../types/dbTypes";
|
||||||
|
import {
|
||||||
|
ReserveTransaction,
|
||||||
|
ReserveTransactionType,
|
||||||
|
} from "../types/ReserveTransaction";
|
||||||
|
import { Amounts } from "./amounts";
|
||||||
|
|
||||||
|
test("basics", (t) => {
|
||||||
|
const r = reconcileReserveHistory([], []);
|
||||||
|
t.deepEqual(r.updatedLocalHistory, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unmatched credit", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 1);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unmatched credit #2", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:50",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC02",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matched credit", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
matchedExchangeTransaction: {
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:50",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC02",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fulfilling credit", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:50",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC02",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unfulfilled credit", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:50",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC02",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("awaited credit", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withdrawal new match", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
matchedExchangeTransaction: {
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Withdraw,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Withdraw,
|
||||||
|
amount: "TESTKUDOS:5",
|
||||||
|
h_coin_envelope: "foobar",
|
||||||
|
h_denom_pub: "foobar",
|
||||||
|
reserve_sig: "foobar",
|
||||||
|
withdraw_fee: "TESTKUDOS:0.1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
console.log(r);
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("claimed but now arrived", (t) => {
|
||||||
|
const localHistory: WalletReserveHistoryItem[] = [
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
|
||||||
|
matchedExchangeTransaction: {
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: WalletReserveHistoryItemType.Withdraw,
|
||||||
|
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const remoteHistory: ReserveTransaction[] = [
|
||||||
|
{
|
||||||
|
type: ReserveTransactionType.Credit,
|
||||||
|
amount: "TESTKUDOS:100",
|
||||||
|
sender_account_url: "payto://void/",
|
||||||
|
timestamp: { t_ms: 42 },
|
||||||
|
wire_reference: "ABC01",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const r = reconcileReserveHistory(localHistory, remoteHistory);
|
||||||
|
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
|
||||||
|
t.deepEqual(r.updatedLocalHistory.length, 2);
|
||||||
|
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
|
||||||
|
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
|
||||||
|
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
|
||||||
|
});
|
384
src/util/reserveHistoryUtil.ts
Normal file
384
src/util/reserveHistoryUtil.ts
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 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 {
|
||||||
|
WalletReserveHistoryItem,
|
||||||
|
WalletReserveHistoryItemType,
|
||||||
|
} from "../types/dbTypes";
|
||||||
|
import {
|
||||||
|
ReserveTransaction,
|
||||||
|
ReserveTransactionType,
|
||||||
|
} from "../types/ReserveTransaction";
|
||||||
|
import * as Amounts from "../util/amounts";
|
||||||
|
import { timestampCmp } from "./time";
|
||||||
|
import { deepCopy } from "./helpers";
|
||||||
|
import { AmountString } from "../types/talerTypes";
|
||||||
|
import { AmountJson } from "../util/amounts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers for dealing with reserve histories.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ReserveReconciliationResult {
|
||||||
|
/**
|
||||||
|
* The wallet's local history reconciled with the exchange's reserve history.
|
||||||
|
*/
|
||||||
|
updatedLocalHistory: WalletReserveHistoryItem[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History items that were newly created, subset of the
|
||||||
|
* updatedLocalHistory items.
|
||||||
|
*/
|
||||||
|
newAddedItems: WalletReserveHistoryItem[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History items that were newly matched, subset of the
|
||||||
|
* updatedLocalHistory items.
|
||||||
|
*/
|
||||||
|
newMatchedItems: WalletReserveHistoryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveHistorySummary {
|
||||||
|
/**
|
||||||
|
* Balance computed by the wallet, should match the balance
|
||||||
|
* computed by the reserve.
|
||||||
|
*/
|
||||||
|
computedReserveBalance: Amounts.AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserve balance that is still available for withdrawal.
|
||||||
|
*/
|
||||||
|
unclaimedReserveAmount: Amounts.AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that we're still expecting to come into the reserve.
|
||||||
|
*/
|
||||||
|
awaitedReserveAmount: Amounts.AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount withdrawn from the reserve so far. Only counts
|
||||||
|
* finished withdrawals, not withdrawals in progress.
|
||||||
|
*/
|
||||||
|
withdrawnAmount: Amounts.AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRemoteHistoryMatch(
|
||||||
|
t1: ReserveTransaction,
|
||||||
|
t2: ReserveTransaction,
|
||||||
|
): boolean {
|
||||||
|
switch (t1.type) {
|
||||||
|
case ReserveTransactionType.Closing: {
|
||||||
|
return t1.type === t2.type && t1.wtid == t2.wtid;
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Credit: {
|
||||||
|
return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Recoup: {
|
||||||
|
return (
|
||||||
|
t1.type === t2.type &&
|
||||||
|
t1.coin_pub === t2.coin_pub &&
|
||||||
|
timestampCmp(t1.timestamp, t2.timestamp) === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Withdraw: {
|
||||||
|
return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalRemoteHistoryPreferredMatch(
|
||||||
|
t1: WalletReserveHistoryItem,
|
||||||
|
t2: ReserveTransaction,
|
||||||
|
): boolean {
|
||||||
|
switch (t1.type) {
|
||||||
|
case WalletReserveHistoryItemType.Credit: {
|
||||||
|
return (
|
||||||
|
t2.type === ReserveTransactionType.Credit &&
|
||||||
|
!!t1.expectedAmount &&
|
||||||
|
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case WalletReserveHistoryItemType.Withdraw:
|
||||||
|
return (
|
||||||
|
t2.type === ReserveTransactionType.Withdraw &&
|
||||||
|
!!t1.expectedAmount &&
|
||||||
|
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
|
||||||
|
)
|
||||||
|
case WalletReserveHistoryItemType.Recoup: {
|
||||||
|
return (
|
||||||
|
t2.type === ReserveTransactionType.Recoup &&
|
||||||
|
!!t1.expectedAmount &&
|
||||||
|
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalRemoteHistoryAcceptableMatch(
|
||||||
|
t1: WalletReserveHistoryItem,
|
||||||
|
t2: ReserveTransaction,
|
||||||
|
): boolean {
|
||||||
|
switch (t1.type) {
|
||||||
|
case WalletReserveHistoryItemType.Closing:
|
||||||
|
throw Error("invariant violated");
|
||||||
|
case WalletReserveHistoryItemType.Credit:
|
||||||
|
return !t1.expectedAmount && t2.type == ReserveTransactionType.Credit;
|
||||||
|
case WalletReserveHistoryItemType.Recoup:
|
||||||
|
return !t1.expectedAmount && t2.type == ReserveTransactionType.Recoup;
|
||||||
|
case WalletReserveHistoryItemType.Withdraw:
|
||||||
|
return !t1.expectedAmount && t2.type == ReserveTransactionType.Withdraw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeReserveHistory(
|
||||||
|
localHistory: WalletReserveHistoryItem[],
|
||||||
|
currency: string,
|
||||||
|
): ReserveHistorySummary {
|
||||||
|
const posAmounts: AmountJson[] = [];
|
||||||
|
const negAmounts: AmountJson[] = [];
|
||||||
|
const expectedPosAmounts: AmountJson[] = [];
|
||||||
|
const expectedNegAmounts: AmountJson[] = [];
|
||||||
|
const withdrawnAmounts: AmountJson[] = [];
|
||||||
|
|
||||||
|
for (const item of localHistory) {
|
||||||
|
switch (item.type) {
|
||||||
|
case WalletReserveHistoryItemType.Credit:
|
||||||
|
if (item.matchedExchangeTransaction) {
|
||||||
|
posAmounts.push(
|
||||||
|
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
|
||||||
|
);
|
||||||
|
} else if (item.expectedAmount) {
|
||||||
|
expectedPosAmounts.push(item.expectedAmount);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WalletReserveHistoryItemType.Recoup:
|
||||||
|
if (item.matchedExchangeTransaction) {
|
||||||
|
if (item.matchedExchangeTransaction) {
|
||||||
|
posAmounts.push(
|
||||||
|
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
|
||||||
|
);
|
||||||
|
} else if (item.expectedAmount) {
|
||||||
|
expectedPosAmounts.push(item.expectedAmount);
|
||||||
|
} else {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WalletReserveHistoryItemType.Closing:
|
||||||
|
if (item.matchedExchangeTransaction) {
|
||||||
|
negAmounts.push(
|
||||||
|
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WalletReserveHistoryItemType.Withdraw:
|
||||||
|
if (item.matchedExchangeTransaction) {
|
||||||
|
negAmounts.push(
|
||||||
|
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
|
||||||
|
);
|
||||||
|
withdrawnAmounts.push(Amounts.parseOrThrow(item.matchedExchangeTransaction.amount));
|
||||||
|
} else if (item.expectedAmount) {
|
||||||
|
expectedNegAmounts.push(item.expectedAmount);
|
||||||
|
} else {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const z = Amounts.getZero(currency);
|
||||||
|
|
||||||
|
const computedBalance = Amounts.sub(
|
||||||
|
Amounts.add(z, ...posAmounts).amount,
|
||||||
|
...negAmounts,
|
||||||
|
).amount;
|
||||||
|
|
||||||
|
const unclaimedReserveAmount = Amounts.sub(
|
||||||
|
Amounts.add(z, ...posAmounts).amount,
|
||||||
|
...negAmounts,
|
||||||
|
...expectedNegAmounts,
|
||||||
|
).amount;
|
||||||
|
|
||||||
|
const awaitedReserveAmount = Amounts.sub(
|
||||||
|
Amounts.add(z, ...expectedPosAmounts).amount,
|
||||||
|
...expectedNegAmounts,
|
||||||
|
).amount;
|
||||||
|
|
||||||
|
const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
computedReserveBalance: computedBalance,
|
||||||
|
unclaimedReserveAmount: unclaimedReserveAmount,
|
||||||
|
awaitedReserveAmount: awaitedReserveAmount,
|
||||||
|
withdrawnAmount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reconcileReserveHistory(
|
||||||
|
localHistory: WalletReserveHistoryItem[],
|
||||||
|
remoteHistory: ReserveTransaction[],
|
||||||
|
): ReserveReconciliationResult {
|
||||||
|
const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
|
||||||
|
localHistory,
|
||||||
|
);
|
||||||
|
const newMatchedItems: WalletReserveHistoryItem[] = [];
|
||||||
|
const newAddedItems: WalletReserveHistoryItem[] = [];
|
||||||
|
|
||||||
|
const remoteMatched = remoteHistory.map(() => false);
|
||||||
|
const localMatched = localHistory.map(() => false);
|
||||||
|
|
||||||
|
// Take care of deposits
|
||||||
|
|
||||||
|
// First, see which pairs are already a definite match.
|
||||||
|
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
|
||||||
|
const rhi = remoteHistory[remoteIndex];
|
||||||
|
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
|
||||||
|
if (localMatched[localIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lhi = localHistory[localIndex];
|
||||||
|
if (!lhi.matchedExchangeTransaction) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
|
||||||
|
localMatched[localIndex] = true;
|
||||||
|
remoteMatched[remoteIndex] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all previously matched items are still matched
|
||||||
|
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
|
||||||
|
if (localMatched[localIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lhi = localHistory[localIndex];
|
||||||
|
if (lhi.matchedExchangeTransaction) {
|
||||||
|
// Don't use for further matching
|
||||||
|
localMatched[localIndex] = true;
|
||||||
|
// FIXME: emit some error here!
|
||||||
|
throw Error("previously matched reserve history item now unmatched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, find out if there are any exact new matches between local and remote
|
||||||
|
// history items
|
||||||
|
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
|
||||||
|
if (localMatched[localIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lhi = localHistory[localIndex];
|
||||||
|
for (
|
||||||
|
let remoteIndex = 0;
|
||||||
|
remoteIndex < remoteHistory.length;
|
||||||
|
remoteIndex++
|
||||||
|
) {
|
||||||
|
const rhi = remoteHistory[remoteIndex];
|
||||||
|
if (remoteMatched[remoteIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isLocalRemoteHistoryPreferredMatch(lhi, rhi)) {
|
||||||
|
localMatched[localIndex] = true;
|
||||||
|
remoteMatched[remoteIndex] = true;
|
||||||
|
updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
|
||||||
|
newMatchedItems.push(lhi);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, find out if there are any acceptable new matches between local and remote
|
||||||
|
// history items
|
||||||
|
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
|
||||||
|
if (localMatched[localIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lhi = localHistory[localIndex];
|
||||||
|
for (
|
||||||
|
let remoteIndex = 0;
|
||||||
|
remoteIndex < remoteHistory.length;
|
||||||
|
remoteIndex++
|
||||||
|
) {
|
||||||
|
const rhi = remoteHistory[remoteIndex];
|
||||||
|
if (remoteMatched[remoteIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isLocalRemoteHistoryAcceptableMatch(lhi, rhi)) {
|
||||||
|
localMatched[localIndex] = true;
|
||||||
|
remoteMatched[remoteIndex] = true;
|
||||||
|
updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
|
||||||
|
newMatchedItems.push(lhi);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally we add new history items
|
||||||
|
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
|
||||||
|
if (remoteMatched[remoteIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rhi = remoteHistory[remoteIndex];
|
||||||
|
let newItem: WalletReserveHistoryItem;
|
||||||
|
switch (rhi.type) {
|
||||||
|
case ReserveTransactionType.Closing: {
|
||||||
|
newItem = {
|
||||||
|
type: WalletReserveHistoryItemType.Closing,
|
||||||
|
matchedExchangeTransaction: rhi,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Credit: {
|
||||||
|
newItem = {
|
||||||
|
type: WalletReserveHistoryItemType.Credit,
|
||||||
|
matchedExchangeTransaction: rhi,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Recoup: {
|
||||||
|
newItem = {
|
||||||
|
type: WalletReserveHistoryItemType.Recoup,
|
||||||
|
matchedExchangeTransaction: rhi,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ReserveTransactionType.Withdraw: {
|
||||||
|
newItem = {
|
||||||
|
type: WalletReserveHistoryItemType.Withdraw,
|
||||||
|
matchedExchangeTransaction: rhi,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedLocalHistory.push(newItem);
|
||||||
|
newAddedItems.push(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedLocalHistory,
|
||||||
|
newAddedItems,
|
||||||
|
newMatchedItems,
|
||||||
|
};
|
||||||
|
}
|
@ -26,8 +26,7 @@ import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
|||||||
import { HttpRequestLibrary } from "./util/http";
|
import { HttpRequestLibrary } from "./util/http";
|
||||||
import { Database } from "./util/query";
|
import { Database } from "./util/query";
|
||||||
|
|
||||||
import { AmountJson } from "./util/amounts";
|
import { Amounts, AmountJson } from "./util/amounts";
|
||||||
import * as Amounts from "./util/amounts";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getWithdrawDetailsForUri,
|
getWithdrawDetailsForUri,
|
||||||
@ -92,7 +91,7 @@ import {
|
|||||||
import { InternalWalletState } from "./operations/state";
|
import { InternalWalletState } from "./operations/state";
|
||||||
import { createReserve, confirmReserve } from "./operations/reserves";
|
import { createReserve, confirmReserve } from "./operations/reserves";
|
||||||
import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
|
import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
|
||||||
import { processWithdrawSession } from "./operations/withdraw";
|
import { processWithdrawGroup } from "./operations/withdraw";
|
||||||
import { getHistory } from "./operations/history";
|
import { getHistory } from "./operations/history";
|
||||||
import { getPendingOperations } from "./operations/pending";
|
import { getPendingOperations } from "./operations/pending";
|
||||||
import { getBalances } from "./operations/balance";
|
import { getBalances } from "./operations/balance";
|
||||||
@ -193,9 +192,9 @@ export class Wallet {
|
|||||||
await processReserve(this.ws, pending.reservePub, forceNow);
|
await processReserve(this.ws, pending.reservePub, forceNow);
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.Withdraw:
|
case PendingOperationType.Withdraw:
|
||||||
await processWithdrawSession(
|
await processWithdrawGroup(
|
||||||
this.ws,
|
this.ws,
|
||||||
pending.withdrawSessionId,
|
pending.withdrawalGroupId,
|
||||||
forceNow,
|
forceNow,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -574,10 +573,14 @@ export class Wallet {
|
|||||||
await this.db.put(Stores.currencies, currencyRecord);
|
await this.db.put(Stores.currencies, currencyRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
|
||||||
return await this.db
|
if (exchangeBaseUrl) {
|
||||||
.iter(Stores.reserves)
|
return await this.db
|
||||||
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
|
.iter(Stores.reserves)
|
||||||
|
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
|
||||||
|
} else {
|
||||||
|
return await this.db.iter(Stores.reserves).toArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
||||||
@ -807,8 +810,8 @@ export class Wallet {
|
|||||||
let withdrawalReservePub: string | undefined;
|
let withdrawalReservePub: string | undefined;
|
||||||
if (cs.type == CoinSourceType.Withdraw) {
|
if (cs.type == CoinSourceType.Withdraw) {
|
||||||
const ws = await this.db.get(
|
const ws = await this.db.get(
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalGroups,
|
||||||
cs.withdrawSessionId,
|
cs.withdrawalGroupId,
|
||||||
);
|
);
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
console.error("no withdrawal session found for coin");
|
console.error("no withdrawal session found for coin");
|
||||||
@ -822,10 +825,10 @@ export class Wallet {
|
|||||||
coin_pub: c.coinPub,
|
coin_pub: c.coinPub,
|
||||||
denom_pub: c.denomPub,
|
denom_pub: c.denomPub,
|
||||||
denom_pub_hash: c.denomPubHash,
|
denom_pub_hash: c.denomPubHash,
|
||||||
denom_value: Amounts.toString(denom.value),
|
denom_value: Amounts.stringify(denom.value),
|
||||||
exchange_base_url: c.exchangeBaseUrl,
|
exchange_base_url: c.exchangeBaseUrl,
|
||||||
refresh_parent_coin_pub: refreshParentCoinPub,
|
refresh_parent_coin_pub: refreshParentCoinPub,
|
||||||
remaining_value: Amounts.toString(c.currentAmount),
|
remaining_value: Amounts.stringify(c.currentAmount),
|
||||||
withdrawal_reserve_pub: withdrawalReservePub,
|
withdrawal_reserve_pub: withdrawalReservePub,
|
||||||
coin_suspended: c.suspended,
|
coin_suspended: c.suspended,
|
||||||
});
|
});
|
||||||
|
@ -565,10 +565,6 @@ function formatHistoryItem(historyItem: HistoryEvent) {
|
|||||||
<HistoryItem
|
<HistoryItem
|
||||||
timestamp={historyItem.timestamp}
|
timestamp={historyItem.timestamp}
|
||||||
small={i18n.str`Reserve balance updated`}
|
small={i18n.str`Reserve balance updated`}
|
||||||
fees={amountDiff(
|
|
||||||
historyItem.amountExpected,
|
|
||||||
historyItem.amountReserveBalance,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AmountJson } from "../../util/amounts";
|
import { AmountJson } from "../../util/amounts";
|
||||||
import * as Amounts from "../../util/amounts";
|
import { Amounts } from "../../util/amounts";
|
||||||
|
|
||||||
import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
|
import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ class ReturnSelectionItem extends React.Component<
|
|||||||
);
|
);
|
||||||
this.state = {
|
this.state = {
|
||||||
currency: props.balance.byExchange[props.exchangeUrl].available.currency,
|
currency: props.balance.byExchange[props.exchangeUrl].available.currency,
|
||||||
selectedValue: Amounts.toString(
|
selectedValue: Amounts.stringify(
|
||||||
props.balance.byExchange[props.exchangeUrl].available,
|
props.balance.byExchange[props.exchangeUrl].available,
|
||||||
),
|
),
|
||||||
selectedWire: "",
|
selectedWire: "",
|
||||||
|
Loading…
Reference in New Issue
Block a user