-refunds for deposit aborts
This commit is contained in:
parent
974cd02066
commit
e4407f8259
@ -2120,3 +2120,47 @@ export interface MerchantUsingTemplateDetails {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
amount?: AmountString;
|
amount?: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangeRefundRequest {
|
||||||
|
// Amount to be refunded, can be a fraction of the
|
||||||
|
// coin's total deposit value (including deposit fee);
|
||||||
|
// must be larger than the refund fee.
|
||||||
|
refund_amount: AmountString;
|
||||||
|
|
||||||
|
// SHA-512 hash of the contact of the merchant with the customer.
|
||||||
|
h_contract_terms: HashCodeString;
|
||||||
|
|
||||||
|
// 64-bit transaction id of the refund transaction between merchant and customer.
|
||||||
|
rtransaction_id: number;
|
||||||
|
|
||||||
|
// EdDSA public key of the merchant.
|
||||||
|
merchant_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// EdDSA signature of the merchant over a
|
||||||
|
// TALER_RefundRequestPS with purpose
|
||||||
|
// TALER_SIGNATURE_MERCHANT_REFUND
|
||||||
|
// affirming the refund.
|
||||||
|
merchant_sig: EddsaPublicKeyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeRefundSuccessResponse {
|
||||||
|
// The EdDSA :ref:signature (binary-only) with purpose
|
||||||
|
// TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
|
||||||
|
// a TALER_RecoupRefreshConfirmationPS
|
||||||
|
// using a current signing key of the
|
||||||
|
// exchange affirming the successful refund.
|
||||||
|
exchange_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// Public EdDSA key of the exchange that was used to generate the signature.
|
||||||
|
// Should match one of the exchange's signing keys from /keys. It is given
|
||||||
|
// explicitly as the client might otherwise be confused by clock skew as to
|
||||||
|
// which signing key was used.
|
||||||
|
exchange_pub: EddsaPublicKeyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForExchangeRefundSuccessResponse =
|
||||||
|
(): Codec<ExchangeRefundSuccessResponse> =>
|
||||||
|
buildCodecForObject<ExchangeRefundSuccessResponse>()
|
||||||
|
.property("exchange_pub", codecForString())
|
||||||
|
.property("exchange_sig", codecForString())
|
||||||
|
.build("ExchangeRefundSuccessResponse");
|
||||||
|
@ -668,6 +668,7 @@ export enum RefreshReason {
|
|||||||
PayPeerPull = "pay-peer-pull",
|
PayPeerPull = "pay-peer-pull",
|
||||||
Refund = "refund",
|
Refund = "refund",
|
||||||
AbortPay = "abort-pay",
|
AbortPay = "abort-pay",
|
||||||
|
AbortDeposit = "abort-deposit",
|
||||||
Recoup = "recoup",
|
Recoup = "recoup",
|
||||||
BackupRestored = "backup-restored",
|
BackupRestored = "backup-restored",
|
||||||
Scheduled = "scheduled",
|
Scheduled = "scheduled",
|
||||||
|
@ -856,6 +856,7 @@ export enum RefreshOperationStatus {
|
|||||||
Pending = 10 /* ACTIVE_START */,
|
Pending = 10 /* ACTIVE_START */,
|
||||||
Finished = 50 /* DORMANT_START */,
|
Finished = 50 /* DORMANT_START */,
|
||||||
FinishedWithError = 51 /* DORMANT_START + 1 */,
|
FinishedWithError = 51 /* DORMANT_START + 1 */,
|
||||||
|
Suspended = 52 /* DORMANT_START + 2 */,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DepositGroupOperationStatus {
|
export enum DepositGroupOperationStatus {
|
||||||
@ -1649,6 +1650,19 @@ export enum DepositOperationStatus {
|
|||||||
Aborting = 11 /* OperationStatusRange.ACTIVE_START + 1 */,
|
Aborting = 11 /* OperationStatusRange.ACTIVE_START + 1 */,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DepositTrackingInfo {
|
||||||
|
// Raw wire transfer identifier of the deposit.
|
||||||
|
wireTransferId: string;
|
||||||
|
// When was the wire transfer given to the bank.
|
||||||
|
timestampExecuted: TalerProtocolTimestamp;
|
||||||
|
// Total amount transfer for this wtid (including fees)
|
||||||
|
amountRaw: AmountString;
|
||||||
|
// Wire fee amount for this exchange
|
||||||
|
wireFee: AmountString;
|
||||||
|
|
||||||
|
exchangePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group of deposits made by the wallet.
|
* Group of deposits made by the wallet.
|
||||||
*/
|
*/
|
||||||
@ -1711,17 +1725,7 @@ export interface DepositGroupRecord {
|
|||||||
|
|
||||||
// FIXME: Do we need this and should it be in this object store?
|
// FIXME: Do we need this and should it be in this object store?
|
||||||
trackingState?: {
|
trackingState?: {
|
||||||
[signature: string]: {
|
[signature: string]: DepositTrackingInfo;
|
||||||
// Raw wire transfer identifier of the deposit.
|
|
||||||
wireTransferId: string;
|
|
||||||
// When was the wire transfer given to the bank.
|
|
||||||
timestampExecuted: TalerProtocolTimestamp;
|
|
||||||
// Total amount transfer for this wtid (including fees)
|
|
||||||
amountRaw: AmountString;
|
|
||||||
// Wire fee amount for this exchange
|
|
||||||
wireFee: AmountString;
|
|
||||||
exchangePub: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ export async function spendCoins(
|
|||||||
tx,
|
tx,
|
||||||
Amounts.currencyOf(csi.contributions[0]),
|
Amounts.currencyOf(csi.contributions[0]),
|
||||||
refreshCoinPubs,
|
refreshCoinPubs,
|
||||||
RefreshReason.PayMerchant,
|
csi.refreshReason,
|
||||||
{
|
{
|
||||||
originatingTransactionId: csi.allocationId,
|
originatingTransactionId: csi.allocationId,
|
||||||
},
|
},
|
||||||
|
@ -27,12 +27,14 @@ import {
|
|||||||
codecForTackTransactionAccepted,
|
codecForTackTransactionAccepted,
|
||||||
codecForTackTransactionWired,
|
codecForTackTransactionWired,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
|
CoinRefreshRequest,
|
||||||
CreateDepositGroupRequest,
|
CreateDepositGroupRequest,
|
||||||
CreateDepositGroupResponse,
|
CreateDepositGroupResponse,
|
||||||
DepositGroupFees,
|
DepositGroupFees,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
ExchangeDepositRequest,
|
ExchangeDepositRequest,
|
||||||
|
ExchangeRefundRequest,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
hashTruncate32,
|
hashTruncate32,
|
||||||
hashWire,
|
hashWire,
|
||||||
@ -65,11 +67,14 @@ import {
|
|||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { TalerError } from "@gnu-taler/taler-util";
|
import { TalerError } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
|
createRefreshGroup,
|
||||||
DepositOperationStatus,
|
DepositOperationStatus,
|
||||||
|
DepositTrackingInfo,
|
||||||
getTotalRefreshCost,
|
getTotalRefreshCost,
|
||||||
KycPendingInfo,
|
KycPendingInfo,
|
||||||
KycUserType,
|
KycUserType,
|
||||||
PendingTaskType,
|
PendingTaskType,
|
||||||
|
RefreshOperationStatus,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||||
@ -88,6 +93,7 @@ import {
|
|||||||
stopLongpolling,
|
stopLongpolling,
|
||||||
} from "./transactions.js";
|
} from "./transactions.js";
|
||||||
import { constructTaskIdentifier } from "../util/retries.js";
|
import { constructTaskIdentifier } from "../util/retries.js";
|
||||||
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -126,6 +132,9 @@ export function computeDepositTransactionStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`num total ${numTotal}`);
|
||||||
|
logger.info(`num deposited ${numDeposited}`);
|
||||||
|
|
||||||
if (numKycRequired > 0) {
|
if (numKycRequired > 0) {
|
||||||
return {
|
return {
|
||||||
major: TransactionMajorState.Pending,
|
major: TransactionMajorState.Pending,
|
||||||
@ -351,6 +360,184 @@ async function checkDepositKycStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the refresh associated with the
|
||||||
|
* aborting deposit group is done.
|
||||||
|
*
|
||||||
|
* If done, mark the deposit transaction as aborted.
|
||||||
|
*
|
||||||
|
* Otherwise continue waiting.
|
||||||
|
*
|
||||||
|
* FIXME: Wait for the refresh group notifications instead of periodically
|
||||||
|
* checking the refresh group status.
|
||||||
|
* FIXME: This is just one transaction, can't we do this in the initial
|
||||||
|
* transaction of processDepositGroup?
|
||||||
|
*/
|
||||||
|
async function waitForRefreshOnDepositGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroup: DepositGroupRecord,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
|
||||||
|
checkLogicInvariant(!!abortRefreshGroupId);
|
||||||
|
// FIXME: Emit notification on state transition!
|
||||||
|
const res = await ws.db
|
||||||
|
.mktx((x) => [x.refreshGroups, x.depositGroups])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
|
||||||
|
let newOpState: DepositOperationStatus | undefined;
|
||||||
|
if (!refreshGroup) {
|
||||||
|
// Maybe it got manually deleted? Means that we should
|
||||||
|
// just go into aborted.
|
||||||
|
logger.warn("no aborting refresh group found for deposit group");
|
||||||
|
newOpState = DepositOperationStatus.Aborted;
|
||||||
|
} else {
|
||||||
|
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
|
||||||
|
newOpState = DepositOperationStatus.Aborted;
|
||||||
|
} else if (
|
||||||
|
refreshGroup.operationStatus ===
|
||||||
|
RefreshOperationStatus.FinishedWithError
|
||||||
|
) {
|
||||||
|
newOpState = DepositOperationStatus.Aborted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newOpState) {
|
||||||
|
const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
|
||||||
|
if (!newDg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldDepositTxStatus = computeDepositTransactionStatus(newDg);
|
||||||
|
newDg.operationStatus = newOpState;
|
||||||
|
const newDepositTxStatus = computeDepositTransactionStatus(newDg);
|
||||||
|
await tx.depositGroups.put(newDg);
|
||||||
|
return { oldDepositTxStatus, newDepositTxStatus };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Deposit,
|
||||||
|
depositGroupId: depositGroup.depositGroupId,
|
||||||
|
});
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.TransactionStateTransition,
|
||||||
|
transactionId,
|
||||||
|
oldTxState: res.oldDepositTxStatus,
|
||||||
|
newTxState: res.newDepositTxStatus,
|
||||||
|
});
|
||||||
|
return OperationAttemptResult.pendingEmpty();
|
||||||
|
} else {
|
||||||
|
return OperationAttemptResult.pendingEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refundDepositGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroup: DepositGroupRecord,
|
||||||
|
): Promise<OperationAttemptResult> {
|
||||||
|
const newTxPerCoin = [...depositGroup.transactionPerCoin];
|
||||||
|
for (let i = 0; i < depositGroup.transactionPerCoin.length; i++) {
|
||||||
|
const st = depositGroup.transactionPerCoin[i];
|
||||||
|
switch (st) {
|
||||||
|
case DepositElementStatus.RefundFailed:
|
||||||
|
case DepositElementStatus.RefundSuccess:
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const coinPub = depositGroup.payCoinSelection.coinPubs[i];
|
||||||
|
const coinExchange = await ws.db
|
||||||
|
.mktx((x) => [x.coins])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const coinRecord = await tx.coins.get(coinPub);
|
||||||
|
checkDbInvariant(!!coinRecord);
|
||||||
|
return coinRecord.exchangeBaseUrl;
|
||||||
|
});
|
||||||
|
const refundAmount = depositGroup.payCoinSelection.coinContributions[i];
|
||||||
|
// We use a constant refund transaction ID, since there can
|
||||||
|
// only be one refund.
|
||||||
|
const rtid = 1;
|
||||||
|
const sig = await ws.cryptoApi.signRefund({
|
||||||
|
coinPub,
|
||||||
|
contractTermsHash: depositGroup.contractTermsHash,
|
||||||
|
merchantPriv: depositGroup.merchantPriv,
|
||||||
|
merchantPub: depositGroup.merchantPub,
|
||||||
|
refundAmount: refundAmount,
|
||||||
|
rtransactionId: rtid,
|
||||||
|
});
|
||||||
|
const refundReq: ExchangeRefundRequest = {
|
||||||
|
h_contract_terms: depositGroup.contractTermsHash,
|
||||||
|
merchant_pub: depositGroup.merchantPub,
|
||||||
|
merchant_sig: sig.sig,
|
||||||
|
refund_amount: refundAmount,
|
||||||
|
rtransaction_id: rtid,
|
||||||
|
};
|
||||||
|
const refundUrl = new URL(`coins/${coinPub}/refund`, coinExchange);
|
||||||
|
const httpResp = await ws.http.fetch(refundUrl.href, {
|
||||||
|
method: "POST",
|
||||||
|
body: refundReq,
|
||||||
|
});
|
||||||
|
let newStatus: DepositElementStatus;
|
||||||
|
if (httpResp.status === 200) {
|
||||||
|
// FIXME: validate response
|
||||||
|
newStatus = DepositElementStatus.RefundSuccess;
|
||||||
|
} else {
|
||||||
|
// FIXME: Store problem somewhere!
|
||||||
|
newStatus = DepositElementStatus.RefundFailed;
|
||||||
|
}
|
||||||
|
// FIXME: Handle case where refund request needs to be tried again
|
||||||
|
newTxPerCoin[i] = newStatus;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let isDone = true;
|
||||||
|
for (let i = 0; i < newTxPerCoin.length; i++) {
|
||||||
|
if (
|
||||||
|
newTxPerCoin[i] != DepositElementStatus.RefundFailed ||
|
||||||
|
newTxPerCoin[i] != DepositElementStatus.RefundSuccess
|
||||||
|
) {
|
||||||
|
isDone = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currency = Amounts.currencyOf(depositGroup.totalPayCost);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.depositGroups,
|
||||||
|
x.refreshGroups,
|
||||||
|
x.coins,
|
||||||
|
x.denominations,
|
||||||
|
x.coinAvailability,
|
||||||
|
])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
|
||||||
|
if (!newDg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newDg.transactionPerCoin = newTxPerCoin;
|
||||||
|
const refreshCoins: CoinRefreshRequest[] = [];
|
||||||
|
for (let i = 0; i < newTxPerCoin.length; i++) {
|
||||||
|
refreshCoins.push({
|
||||||
|
amount: depositGroup.payCoinSelection.coinContributions[i],
|
||||||
|
coinPub: depositGroup.payCoinSelection.coinPubs[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isDone) {
|
||||||
|
const rgid = await createRefreshGroup(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
|
currency,
|
||||||
|
refreshCoins,
|
||||||
|
RefreshReason.AbortDeposit,
|
||||||
|
);
|
||||||
|
newDg.abortRefreshGroupId = rgid.refreshGroupId;
|
||||||
|
}
|
||||||
|
await tx.depositGroups.put(newDg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return OperationAttemptResult.pendingEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a deposit group that is not in its final state yet.
|
* Process a deposit group that is not in its final state yet.
|
||||||
*/
|
*/
|
||||||
@ -401,7 +588,7 @@ export async function processDepositGroup(
|
|||||||
for (let i = 0; i < depositPermissions.length; i++) {
|
for (let i = 0; i < depositPermissions.length; i++) {
|
||||||
const perm = depositPermissions[i];
|
const perm = depositPermissions[i];
|
||||||
|
|
||||||
let updatedDeposit: boolean = false;
|
let didDeposit: boolean = false;
|
||||||
|
|
||||||
if (!depositGroup.depositedPerCoin[i]) {
|
if (!depositGroup.depositedPerCoin[i]) {
|
||||||
const requestBody: ExchangeDepositRequest = {
|
const requestBody: ExchangeDepositRequest = {
|
||||||
@ -435,16 +622,15 @@ export async function processDepositGroup(
|
|||||||
httpResp,
|
httpResp,
|
||||||
codecForDepositSuccess(),
|
codecForDepositSuccess(),
|
||||||
);
|
);
|
||||||
updatedDeposit = true;
|
didDeposit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedTxStatus: DepositElementStatus | undefined = undefined;
|
let updatedTxStatus: DepositElementStatus | undefined = undefined;
|
||||||
type ValueOf<T> = T[keyof T];
|
|
||||||
|
|
||||||
let newWiredCoin:
|
let newWiredCoin:
|
||||||
| {
|
| {
|
||||||
id: string;
|
id: string;
|
||||||
value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
|
value: DepositTrackingInfo;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
@ -499,7 +685,7 @@ export async function processDepositGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedTxStatus !== undefined || updatedDeposit) {
|
if (updatedTxStatus !== undefined || didDeposit) {
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.depositGroups])
|
.mktx((x) => [x.depositGroups])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -507,8 +693,8 @@ export async function processDepositGroup(
|
|||||||
if (!dg) {
|
if (!dg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (updatedDeposit !== undefined) {
|
if (didDeposit) {
|
||||||
dg.depositedPerCoin[i] = updatedDeposit;
|
dg.depositedPerCoin[i] = didDeposit;
|
||||||
}
|
}
|
||||||
if (updatedTxStatus !== undefined) {
|
if (updatedTxStatus !== undefined) {
|
||||||
dg.transactionPerCoin[i] = updatedTxStatus;
|
dg.transactionPerCoin[i] = updatedTxStatus;
|
||||||
@ -526,7 +712,8 @@ export async function processDepositGroup(
|
|||||||
dg.trackingState = {};
|
dg.trackingState = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
|
dg.trackingState[newWiredCoin.id] =
|
||||||
|
newWiredCoin.value;
|
||||||
}
|
}
|
||||||
await tx.depositGroups.put(dg);
|
await tx.depositGroups.put(dg);
|
||||||
});
|
});
|
||||||
@ -588,10 +775,12 @@ export async function processDepositGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
|
if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
|
||||||
// FIXME: Implement!
|
const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
|
||||||
return OperationAttemptResult.pendingEmpty();
|
if (!abortRefreshGroupId) {
|
||||||
|
return refundDepositGroup(ws, depositGroup);
|
||||||
|
}
|
||||||
|
return waitForRefreshOnDepositGroup(ws, depositGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OperationAttemptResult.finishedEmpty();
|
return OperationAttemptResult.finishedEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,9 @@ import {
|
|||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
|
TransactionMajorState,
|
||||||
|
TransactionState,
|
||||||
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
||||||
@ -80,13 +83,19 @@ import {
|
|||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
|
constructTaskIdentifier,
|
||||||
OperationAttemptResult,
|
OperationAttemptResult,
|
||||||
OperationAttemptResultType,
|
OperationAttemptResultType,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import { makeCoinAvailable } from "./common.js";
|
import { makeCoinAvailable } from "./common.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
|
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
|
||||||
import { isWithdrawableDenom, WalletConfig } from "../index.js";
|
import {
|
||||||
|
isWithdrawableDenom,
|
||||||
|
PendingTaskType,
|
||||||
|
WalletConfig,
|
||||||
|
} from "../index.js";
|
||||||
|
import { constructTransactionIdentifier } from "./transactions.js";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -1115,3 +1124,128 @@ export async function autoRefresh(
|
|||||||
});
|
});
|
||||||
return OperationAttemptResult.finishedEmpty();
|
return OperationAttemptResult.finishedEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function computeRefreshTransactionStatus(
|
||||||
|
rg: RefreshGroupRecord,
|
||||||
|
): TransactionState {
|
||||||
|
switch (rg.operationStatus) {
|
||||||
|
case RefreshOperationStatus.Finished:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Done,
|
||||||
|
};
|
||||||
|
case RefreshOperationStatus.FinishedWithError:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Failed,
|
||||||
|
};
|
||||||
|
case RefreshOperationStatus.Pending:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Pending,
|
||||||
|
};
|
||||||
|
case RefreshOperationStatus.Suspended:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Suspended,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function suspendRefreshGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
const retryTag = constructTaskIdentifier({
|
||||||
|
tag: PendingTaskType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
let res = await ws.db
|
||||||
|
.mktx((x) => [x.refreshGroups])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const dg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
|
if (!dg) {
|
||||||
|
logger.warn(
|
||||||
|
`can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const oldState = computeRefreshTransactionStatus(dg);
|
||||||
|
switch (dg.operationStatus) {
|
||||||
|
case RefreshOperationStatus.Finished:
|
||||||
|
return undefined;
|
||||||
|
case RefreshOperationStatus.Pending: {
|
||||||
|
dg.operationStatus = RefreshOperationStatus.Suspended;
|
||||||
|
await tx.refreshGroups.put(dg);
|
||||||
|
return {
|
||||||
|
oldTxState: oldState,
|
||||||
|
newTxState: computeRefreshTransactionStatus(dg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case RefreshOperationStatus.Suspended:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.TransactionStateTransition,
|
||||||
|
transactionId,
|
||||||
|
oldTxState: res.oldTxState,
|
||||||
|
newTxState: res.newTxState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumeRefreshGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refresh,
|
||||||
|
refreshGroupId,
|
||||||
|
});
|
||||||
|
let res = await ws.db
|
||||||
|
.mktx((x) => [x.refreshGroups])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const dg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
|
if (!dg) {
|
||||||
|
logger.warn(
|
||||||
|
`can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldState = computeRefreshTransactionStatus(dg);
|
||||||
|
switch (dg.operationStatus) {
|
||||||
|
case RefreshOperationStatus.Finished:
|
||||||
|
return;
|
||||||
|
case RefreshOperationStatus.Pending: {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case RefreshOperationStatus.Suspended:
|
||||||
|
dg.operationStatus = RefreshOperationStatus.Pending;
|
||||||
|
await tx.refreshGroups.put(dg);
|
||||||
|
return {
|
||||||
|
oldTxState: oldState,
|
||||||
|
newTxState: computeRefreshTransactionStatus(dg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
ws.latch.trigger();
|
||||||
|
if (res) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.TransactionStateTransition,
|
||||||
|
transactionId,
|
||||||
|
oldTxState: res.oldTxState,
|
||||||
|
newTxState: res.newTxState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortRefreshGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
throw Error("can't abort refresh groups.");
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user