wallet-core: make basic backup work again
This commit is contained in:
parent
859991a40c
commit
28b4489bea
@ -31,9 +31,6 @@ import {
|
||||
} from "../harness/helpers.js";
|
||||
import { SyncService } from "../harness/sync";
|
||||
|
||||
/**
|
||||
* Run test for basic, bank-integrated withdrawal.
|
||||
*/
|
||||
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
@ -131,6 +128,13 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
||||
|
||||
// Make wallet pay for the order
|
||||
|
||||
{
|
||||
console.log(
|
||||
"wallet2 balance before preparePay:",
|
||||
await wallet2.client.call(WalletApiOperation.GetBalances, {}),
|
||||
);
|
||||
}
|
||||
|
||||
const preparePayResult = await wallet2.client.call(
|
||||
WalletApiOperation.PreparePayForUri,
|
||||
{
|
||||
|
@ -113,6 +113,19 @@ function getDefaultHint(code: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
export class TalerProtocolViolationError<T = any> extends Error {
|
||||
constructor(hint?: string) {
|
||||
let msg: string;
|
||||
if (hint) {
|
||||
msg = `Taler protocol violation error (${hint})`;
|
||||
} else {
|
||||
msg = `Taler protocol violation error`;
|
||||
}
|
||||
super(msg);
|
||||
Object.setPrototypeOf(this, TalerProtocolViolationError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class TalerError<T = any> extends Error {
|
||||
errorDetail: TalerErrorDetail & T;
|
||||
private constructor(d: TalerErrorDetail & T) {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
AgeRestriction,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
BackupCoin,
|
||||
BackupCoinSourceType,
|
||||
BackupDenomSel,
|
||||
BackupProposalStatus,
|
||||
@ -37,6 +38,7 @@ import {
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
AbortStatus,
|
||||
CoinRecord,
|
||||
CoinSource,
|
||||
CoinSourceType,
|
||||
CoinStatus,
|
||||
@ -65,6 +67,7 @@ import {
|
||||
} from "../../util/invariants.js";
|
||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||
import { RetryInfo } from "../../util/retries.js";
|
||||
import { makeCoinAvailable } from "../../wallet.js";
|
||||
import { getExchangeDetails } from "../exchanges.js";
|
||||
import { makeEventId, TombstoneTag } from "../transactions.js";
|
||||
import { provideBackupState } from "./state.js";
|
||||
@ -226,6 +229,71 @@ export interface BackupCryptoPrecomputedData {
|
||||
reservePrivToPub: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function importCoin(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
}>,
|
||||
cryptoComp: BackupCryptoPrecomputedData,
|
||||
args: {
|
||||
backupCoin: BackupCoin;
|
||||
exchangeBaseUrl: string;
|
||||
denomPubHash: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const { backupCoin, exchangeBaseUrl, denomPubHash } = args;
|
||||
const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
||||
checkLogicInvariant(!!compCoin);
|
||||
const existingCoin = await tx.coins.get(compCoin.coinPub);
|
||||
if (!existingCoin) {
|
||||
let coinSource: CoinSource;
|
||||
switch (backupCoin.coin_source.type) {
|
||||
case BackupCoinSourceType.Refresh:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Refresh,
|
||||
oldCoinPub: backupCoin.coin_source.old_coin_pub,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Tip:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Tip,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
walletTipId: backupCoin.coin_source.wallet_tip_id,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Withdraw:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Withdraw,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
reservePub: backupCoin.coin_source.reserve_pub,
|
||||
withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
const coinRecord: CoinRecord = {
|
||||
blindingKey: backupCoin.blinding_key,
|
||||
coinEvHash: compCoin.coinEvHash,
|
||||
coinPriv: backupCoin.coin_priv,
|
||||
currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
|
||||
denomSig: backupCoin.denom_sig,
|
||||
coinPub: compCoin.coinPub,
|
||||
exchangeBaseUrl,
|
||||
denomPubHash,
|
||||
status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant,
|
||||
coinSource,
|
||||
// FIXME!
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
};
|
||||
if (coinRecord.status === CoinStatus.Fresh) {
|
||||
await makeCoinAvailable(ws, tx, coinRecord);
|
||||
} else {
|
||||
await tx.coins.put(coinRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function importBackup(
|
||||
ws: InternalWalletState,
|
||||
backupBlobArg: any,
|
||||
@ -241,6 +309,7 @@ export async function importBackup(
|
||||
x.exchangeDetails,
|
||||
x.exchanges,
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.denominations,
|
||||
x.purchases,
|
||||
x.proposals,
|
||||
@ -360,10 +429,6 @@ export async function importBackup(
|
||||
denomPubHash,
|
||||
]);
|
||||
if (!existingDenom) {
|
||||
logger.info(
|
||||
`importing backup denomination: ${j2s(backupDenomination)}`,
|
||||
);
|
||||
|
||||
const value = Amounts.parseOrThrow(backupDenomination.value);
|
||||
|
||||
await tx.denominations.put({
|
||||
@ -398,53 +463,11 @@ export async function importBackup(
|
||||
});
|
||||
}
|
||||
for (const backupCoin of backupDenomination.coins) {
|
||||
const compCoin =
|
||||
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
||||
checkLogicInvariant(!!compCoin);
|
||||
const existingCoin = await tx.coins.get(compCoin.coinPub);
|
||||
if (!existingCoin) {
|
||||
let coinSource: CoinSource;
|
||||
switch (backupCoin.coin_source.type) {
|
||||
case BackupCoinSourceType.Refresh:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Refresh,
|
||||
oldCoinPub: backupCoin.coin_source.old_coin_pub,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Tip:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Tip,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
walletTipId: backupCoin.coin_source.wallet_tip_id,
|
||||
};
|
||||
break;
|
||||
case BackupCoinSourceType.Withdraw:
|
||||
coinSource = {
|
||||
type: CoinSourceType.Withdraw,
|
||||
coinIndex: backupCoin.coin_source.coin_index,
|
||||
reservePub: backupCoin.coin_source.reserve_pub,
|
||||
withdrawalGroupId:
|
||||
backupCoin.coin_source.withdrawal_group_id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
await tx.coins.put({
|
||||
blindingKey: backupCoin.blinding_key,
|
||||
coinEvHash: compCoin.coinEvHash,
|
||||
coinPriv: backupCoin.coin_priv,
|
||||
currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
|
||||
denomSig: backupCoin.denom_sig,
|
||||
coinPub: compCoin.coinPub,
|
||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
denomPubHash,
|
||||
status: backupCoin.fresh
|
||||
? CoinStatus.Fresh
|
||||
: CoinStatus.Dormant,
|
||||
coinSource,
|
||||
// FIXME!
|
||||
maxAge: AgeRestriction.AGE_UNRESTRICTED,
|
||||
});
|
||||
}
|
||||
await importCoin(ws, tx, cryptoComp, {
|
||||
backupCoin,
|
||||
denomPubHash,
|
||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,97 +555,6 @@ export async function importBackup(
|
||||
timestampFinish: backupWg.timestamp_finish,
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: import reserves with new schema
|
||||
|
||||
// for (const backupReserve of backupExchangeDetails.reserves) {
|
||||
// const reservePub =
|
||||
// cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
||||
// const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
||||
// if (tombstoneSet.has(ts)) {
|
||||
// continue;
|
||||
// }
|
||||
// checkLogicInvariant(!!reservePub);
|
||||
// const existingReserve = await tx.reserves.get(reservePub);
|
||||
// const instructedAmount = Amounts.parseOrThrow(
|
||||
// backupReserve.instructed_amount,
|
||||
// );
|
||||
// if (!existingReserve) {
|
||||
// let bankInfo: ReserveBankInfo | undefined;
|
||||
// if (backupReserve.bank_info) {
|
||||
// bankInfo = {
|
||||
// exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
|
||||
// statusUrl: backupReserve.bank_info.status_url,
|
||||
// confirmUrl: backupReserve.bank_info.confirm_url,
|
||||
// };
|
||||
// }
|
||||
// await tx.reserves.put({
|
||||
// currency: instructedAmount.currency,
|
||||
// instructedAmount,
|
||||
// exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
// reservePub,
|
||||
// reservePriv: backupReserve.reserve_priv,
|
||||
// bankInfo,
|
||||
// timestampCreated: backupReserve.timestamp_created,
|
||||
// timestampBankConfirmed:
|
||||
// backupReserve.bank_info?.timestamp_bank_confirmed,
|
||||
// timestampReserveInfoPosted:
|
||||
// backupReserve.bank_info?.timestamp_reserve_info_posted,
|
||||
// senderWire: backupReserve.sender_wire,
|
||||
// retryInfo: RetryInfo.reset(),
|
||||
// lastError: undefined,
|
||||
// initialWithdrawalGroupId:
|
||||
// backupReserve.initial_withdrawal_group_id,
|
||||
// initialWithdrawalStarted:
|
||||
// backupReserve.withdrawal_groups.length > 0,
|
||||
// // FIXME!
|
||||
// reserveStatus: ReserveRecordStatus.QueryingStatus,
|
||||
// initialDenomSel: await getDenomSelStateFromBackup(
|
||||
// tx,
|
||||
// backupExchangeDetails.base_url,
|
||||
// backupReserve.initial_selected_denoms,
|
||||
// ),
|
||||
// // FIXME!
|
||||
// operationStatus: OperationStatus.Pending,
|
||||
// });
|
||||
// }
|
||||
// for (const backupWg of backupReserve.withdrawal_groups) {
|
||||
// const ts = makeEventId(
|
||||
// TombstoneTag.DeleteWithdrawalGroup,
|
||||
// backupWg.withdrawal_group_id,
|
||||
// );
|
||||
// if (tombstoneSet.has(ts)) {
|
||||
// continue;
|
||||
// }
|
||||
// const existingWg = await tx.withdrawalGroups.get(
|
||||
// backupWg.withdrawal_group_id,
|
||||
// );
|
||||
// if (!existingWg) {
|
||||
// await tx.withdrawalGroups.put({
|
||||
// denomsSel: await getDenomSelStateFromBackup(
|
||||
// tx,
|
||||
// backupExchangeDetails.base_url,
|
||||
// backupWg.selected_denoms,
|
||||
// ),
|
||||
// exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||
// lastError: undefined,
|
||||
// rawWithdrawalAmount: Amounts.parseOrThrow(
|
||||
// backupWg.raw_withdrawal_amount,
|
||||
// ),
|
||||
// reservePub,
|
||||
// retryInfo: RetryInfo.reset(),
|
||||
// secretSeed: backupWg.secret_seed,
|
||||
// timestampStart: backupWg.timestamp_created,
|
||||
// timestampFinish: backupWg.timestamp_finish,
|
||||
// withdrawalGroupId: backupWg.withdrawal_group_id,
|
||||
// denomSelUid: backupWg.selected_denoms_id,
|
||||
// operationStatus: backupWg.timestamp_finish
|
||||
// ? OperationStatus.Finished
|
||||
// : OperationStatus.Pending,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
for (const backupProposal of backupBlob.proposals) {
|
||||
|
@ -482,6 +482,8 @@ export async function processBackupForProvider(
|
||||
throw Error("unknown backup provider");
|
||||
}
|
||||
|
||||
logger.info(`running backup for provider ${backupProviderBaseUrl}`);
|
||||
|
||||
return await runBackupCycleForProvider(ws, {
|
||||
backupProviderBaseUrl: provider.baseUrl,
|
||||
retryAfterPayment: true,
|
||||
|
@ -78,6 +78,7 @@ import {
|
||||
makeErrorDetail,
|
||||
makePendingOperationFailedError,
|
||||
TalerError,
|
||||
TalerProtocolViolationError,
|
||||
} from "../errors.js";
|
||||
import {
|
||||
EXCHANGE_COINS_LOCK,
|
||||
@ -752,7 +753,7 @@ async function handleInsufficientFunds(
|
||||
return;
|
||||
}
|
||||
|
||||
const brokenCoinPub = (err as any).coin_pub;
|
||||
logger.trace(`got error details: ${j2s(err)}`);
|
||||
|
||||
const exchangeReply = (err as any).exchange_reply;
|
||||
if (
|
||||
@ -766,7 +767,12 @@ async function handleInsufficientFunds(
|
||||
throw Error(`unable to handle /pay error response (${exchangeReply.code})`);
|
||||
}
|
||||
|
||||
logger.trace(`got error details: ${j2s(err)}`);
|
||||
const brokenCoinPub = (exchangeReply as any).coin_pub;
|
||||
logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
|
||||
|
||||
if (!brokenCoinPub) {
|
||||
throw new TalerProtocolViolationError();
|
||||
}
|
||||
|
||||
const { contractData } = proposal.download;
|
||||
|
||||
@ -1146,6 +1152,8 @@ export async function selectPayCoinsNew(
|
||||
req,
|
||||
);
|
||||
|
||||
// logger.trace(`candidate denoms: ${j2s(candidateDenoms)}`);
|
||||
|
||||
const coinPubs: string[] = [];
|
||||
const coinContributions: AmountJson[] = [];
|
||||
const currency = contractTermsAmount.currency;
|
||||
@ -1201,6 +1209,9 @@ export async function selectPayCoinsNew(
|
||||
|
||||
const finalSel = selectedDenom;
|
||||
|
||||
logger.trace(`coin selection request ${j2s(req)}`);
|
||||
logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.coins, x.denominations])
|
||||
.runReadOnly(async (tx) => {
|
||||
@ -1301,7 +1312,7 @@ export async function checkPaymentByProposalId(
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
logger.info("not confirming payment, insufficient coins");
|
||||
logger.info("not allowing payment, insufficient coins");
|
||||
return {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
contractTerms: d.contractTermsRaw,
|
||||
|
@ -221,7 +221,7 @@ import {
|
||||
HttpRequestLibrary,
|
||||
readSuccessResponseJsonOrThrow,
|
||||
} from "./util/http.js";
|
||||
import { checkDbInvariant } from "./util/invariants.js";
|
||||
import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js";
|
||||
import {
|
||||
AsyncCondition,
|
||||
OpenedPromise,
|
||||
@ -812,6 +812,7 @@ export async function makeCoinAvailable(
|
||||
}>,
|
||||
coinRecord: CoinRecord,
|
||||
): Promise<void> {
|
||||
checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
|
||||
const existingCoin = await tx.coins.get(coinRecord.coinPub);
|
||||
if (existingCoin) {
|
||||
return;
|
||||
|
Loading…
Reference in New Issue
Block a user