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";
|
} from "../harness/helpers.js";
|
||||||
import { SyncService } from "../harness/sync";
|
import { SyncService } from "../harness/sync";
|
||||||
|
|
||||||
/**
|
|
||||||
* Run test for basic, bank-integrated withdrawal.
|
|
||||||
*/
|
|
||||||
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
||||||
// Set up test environment
|
// Set up test environment
|
||||||
|
|
||||||
@ -131,6 +128,13 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
|
|||||||
|
|
||||||
// Make wallet pay for the order
|
// Make wallet pay for the order
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log(
|
||||||
|
"wallet2 balance before preparePay:",
|
||||||
|
await wallet2.client.call(WalletApiOperation.GetBalances, {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const preparePayResult = await wallet2.client.call(
|
const preparePayResult = await wallet2.client.call(
|
||||||
WalletApiOperation.PreparePayForUri,
|
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 {
|
export class TalerError<T = any> extends Error {
|
||||||
errorDetail: TalerErrorDetail & T;
|
errorDetail: TalerErrorDetail & T;
|
||||||
private constructor(d: TalerErrorDetail & T) {
|
private constructor(d: TalerErrorDetail & T) {
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
AgeRestriction,
|
AgeRestriction,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
BackupCoin,
|
||||||
BackupCoinSourceType,
|
BackupCoinSourceType,
|
||||||
BackupDenomSel,
|
BackupDenomSel,
|
||||||
BackupProposalStatus,
|
BackupProposalStatus,
|
||||||
@ -37,6 +38,7 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
|
CoinRecord,
|
||||||
CoinSource,
|
CoinSource,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
@ -65,6 +67,7 @@ import {
|
|||||||
} from "../../util/invariants.js";
|
} from "../../util/invariants.js";
|
||||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||||
import { RetryInfo } from "../../util/retries.js";
|
import { RetryInfo } from "../../util/retries.js";
|
||||||
|
import { makeCoinAvailable } from "../../wallet.js";
|
||||||
import { getExchangeDetails } from "../exchanges.js";
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
import { makeEventId, TombstoneTag } from "../transactions.js";
|
import { makeEventId, TombstoneTag } from "../transactions.js";
|
||||||
import { provideBackupState } from "./state.js";
|
import { provideBackupState } from "./state.js";
|
||||||
@ -226,6 +229,71 @@ export interface BackupCryptoPrecomputedData {
|
|||||||
reservePrivToPub: Record<string, string>;
|
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(
|
export async function importBackup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
backupBlobArg: any,
|
backupBlobArg: any,
|
||||||
@ -241,6 +309,7 @@ export async function importBackup(
|
|||||||
x.exchangeDetails,
|
x.exchangeDetails,
|
||||||
x.exchanges,
|
x.exchanges,
|
||||||
x.coins,
|
x.coins,
|
||||||
|
x.coinAvailability,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
x.purchases,
|
x.purchases,
|
||||||
x.proposals,
|
x.proposals,
|
||||||
@ -360,10 +429,6 @@ export async function importBackup(
|
|||||||
denomPubHash,
|
denomPubHash,
|
||||||
]);
|
]);
|
||||||
if (!existingDenom) {
|
if (!existingDenom) {
|
||||||
logger.info(
|
|
||||||
`importing backup denomination: ${j2s(backupDenomination)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = Amounts.parseOrThrow(backupDenomination.value);
|
const value = Amounts.parseOrThrow(backupDenomination.value);
|
||||||
|
|
||||||
await tx.denominations.put({
|
await tx.denominations.put({
|
||||||
@ -398,53 +463,11 @@ export async function importBackup(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const backupCoin of backupDenomination.coins) {
|
for (const backupCoin of backupDenomination.coins) {
|
||||||
const compCoin =
|
await importCoin(ws, tx, cryptoComp, {
|
||||||
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
backupCoin,
|
||||||
checkLogicInvariant(!!compCoin);
|
denomPubHash,
|
||||||
const existingCoin = await tx.coins.get(compCoin.coinPub);
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,97 +555,6 @@ export async function importBackup(
|
|||||||
timestampFinish: backupWg.timestamp_finish,
|
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) {
|
for (const backupProposal of backupBlob.proposals) {
|
||||||
|
@ -482,6 +482,8 @@ export async function processBackupForProvider(
|
|||||||
throw Error("unknown backup provider");
|
throw Error("unknown backup provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`running backup for provider ${backupProviderBaseUrl}`);
|
||||||
|
|
||||||
return await runBackupCycleForProvider(ws, {
|
return await runBackupCycleForProvider(ws, {
|
||||||
backupProviderBaseUrl: provider.baseUrl,
|
backupProviderBaseUrl: provider.baseUrl,
|
||||||
retryAfterPayment: true,
|
retryAfterPayment: true,
|
||||||
|
@ -78,6 +78,7 @@ import {
|
|||||||
makeErrorDetail,
|
makeErrorDetail,
|
||||||
makePendingOperationFailedError,
|
makePendingOperationFailedError,
|
||||||
TalerError,
|
TalerError,
|
||||||
|
TalerProtocolViolationError,
|
||||||
} from "../errors.js";
|
} from "../errors.js";
|
||||||
import {
|
import {
|
||||||
EXCHANGE_COINS_LOCK,
|
EXCHANGE_COINS_LOCK,
|
||||||
@ -752,7 +753,7 @@ async function handleInsufficientFunds(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const brokenCoinPub = (err as any).coin_pub;
|
logger.trace(`got error details: ${j2s(err)}`);
|
||||||
|
|
||||||
const exchangeReply = (err as any).exchange_reply;
|
const exchangeReply = (err as any).exchange_reply;
|
||||||
if (
|
if (
|
||||||
@ -766,7 +767,12 @@ async function handleInsufficientFunds(
|
|||||||
throw Error(`unable to handle /pay error response (${exchangeReply.code})`);
|
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;
|
const { contractData } = proposal.download;
|
||||||
|
|
||||||
@ -1146,6 +1152,8 @@ export async function selectPayCoinsNew(
|
|||||||
req,
|
req,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// logger.trace(`candidate denoms: ${j2s(candidateDenoms)}`);
|
||||||
|
|
||||||
const coinPubs: string[] = [];
|
const coinPubs: string[] = [];
|
||||||
const coinContributions: AmountJson[] = [];
|
const coinContributions: AmountJson[] = [];
|
||||||
const currency = contractTermsAmount.currency;
|
const currency = contractTermsAmount.currency;
|
||||||
@ -1201,6 +1209,9 @@ export async function selectPayCoinsNew(
|
|||||||
|
|
||||||
const finalSel = selectedDenom;
|
const finalSel = selectedDenom;
|
||||||
|
|
||||||
|
logger.trace(`coin selection request ${j2s(req)}`);
|
||||||
|
logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
|
||||||
|
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.coins, x.denominations])
|
.mktx((x) => [x.coins, x.denominations])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -1301,7 +1312,7 @@ export async function checkPaymentByProposalId(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
logger.info("not confirming payment, insufficient coins");
|
logger.info("not allowing payment, insufficient coins");
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
contractTerms: d.contractTermsRaw,
|
contractTerms: d.contractTermsRaw,
|
||||||
|
@ -221,7 +221,7 @@ import {
|
|||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "./util/http.js";
|
} from "./util/http.js";
|
||||||
import { checkDbInvariant } from "./util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js";
|
||||||
import {
|
import {
|
||||||
AsyncCondition,
|
AsyncCondition,
|
||||||
OpenedPromise,
|
OpenedPromise,
|
||||||
@ -812,6 +812,7 @@ export async function makeCoinAvailable(
|
|||||||
}>,
|
}>,
|
||||||
coinRecord: CoinRecord,
|
coinRecord: CoinRecord,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
|
||||||
const existingCoin = await tx.coins.get(coinRecord.coinPub);
|
const existingCoin = await tx.coins.get(coinRecord.coinPub);
|
||||||
if (existingCoin) {
|
if (existingCoin) {
|
||||||
return;
|
return;
|
||||||
|
Loading…
Reference in New Issue
Block a user