fix tipping and adjust DB

This commit is contained in:
Florian Dold 2020-09-08 19:27:08 +05:30
parent b063382d25
commit b9e43e652e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 166 additions and 173 deletions

View File

@ -94,13 +94,19 @@ runTest(async (t: GlobalTestState) => {
console.log(ptr); console.log(ptr);
t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5");
t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85");
await wallet.acceptTip({ await wallet.acceptTip({
walletTipId: ptr.walletTipId, walletTipId: ptr.walletTipId,
}); });
await wallet.runUntilDone(); await wallet.runUntilDone();
const bal = await wallet.getBalances(); const bal = await wallet.getBalances();
console.log(bal); console.log(bal);
t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85");
}); });

View File

@ -22,6 +22,8 @@
*/ */
export enum TalerErrorCode { export enum TalerErrorCode {
/** /**
* Special code to indicate no error (or no "code" present). * Special code to indicate no error (or no "code" present).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@ -3270,10 +3272,18 @@ export enum TalerErrorCode {
*/ */
WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015, WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015,
/**
* The signature on a coin by the exchange's denomination key (obtained through the merchant via tipping) is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
WALLET_TIPPING_COIN_SIGNATURE_INVALID = 7016,
/** /**
* End of error code range. * End of error code range.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side). * (A value of 0 indicates that the error is generated client-side).
*/ */
END = 9999, END = 9999,
} }

View File

@ -286,7 +286,6 @@ async function gatherWithdrawalPending(
givesLifeness: true, givesLifeness: true,
numCoinsTotal, numCoinsTotal,
numCoinsWithdrawn, numCoinsWithdrawn,
source: wsr.source,
withdrawalGroupId: wsr.withdrawalGroupId, withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError, lastError: wsr.lastError,
retryInfo: wsr.retryInfo, retryInfo: wsr.retryInfo,

View File

@ -818,10 +818,7 @@ async function depleteReserve(
const withdrawalRecord: WithdrawalGroupRecord = { const withdrawalRecord: WithdrawalGroupRecord = {
withdrawalGroupId: withdrawalGroupId, withdrawalGroupId: withdrawalGroupId,
exchangeBaseUrl: newReserve.exchangeBaseUrl, exchangeBaseUrl: newReserve.exchangeBaseUrl,
source: { reservePub: newReserve.reservePub,
type: WithdrawalSourceType.Reserve,
reservePub: newReserve.reservePub,
},
rawWithdrawalAmount: withdrawAmount, rawWithdrawalAmount: withdrawAmount,
timestampStart: getTimestampNow(), timestampStart: getTimestampNow(),
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),

View File

@ -31,6 +31,9 @@ import {
updateRetryInfoTimeout, updateRetryInfoTimeout,
WithdrawalSourceType, WithdrawalSourceType,
TipPlanchet, TipPlanchet,
CoinRecord,
CoinSourceType,
CoinStatus,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
@ -40,13 +43,14 @@ import {
} from "./withdraw"; } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors"; import { guardOperationException, makeErrorDetails } from "./errors";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { getTimestampNow } from "../util/time"; import { getTimestampNow } from "../util/time";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import { readSuccessResponseJsonOrThrow } from "../util/http";
import { URL } from "../util/url"; import { URL } from "../util/url";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
import { TalerErrorCode } from "../TalerErrorCode";
const logger = new Logger("operations/tip.ts"); const logger = new Logger("operations/tip.ts");
@ -99,7 +103,7 @@ export async function prepareTip(
walletTipId: walletTipId, walletTipId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
rejectedTimestamp: undefined, rejectedTimestamp: undefined,
amount, tipAmountRaw: amount,
deadline: tipPickupStatus.expiration, deadline: tipPickupStatus.expiration,
exchangeUrl: tipPickupStatus.exchange_url, exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
@ -109,10 +113,10 @@ export async function prepareTip(
response: undefined, response: undefined,
createdTimestamp: getTimestampNow(), createdTimestamp: getTimestampNow(),
merchantTipId: res.merchantTipId, merchantTipId: res.merchantTipId,
totalFees: Amounts.add( tipAmountEffective: Amounts.sub(amount, Amounts.add(
withdrawDetails.overhead, withdrawDetails.overhead,
withdrawDetails.withdrawFee, withdrawDetails.withdrawFee,
).amount, ).amount).amount,
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
lastError: undefined, lastError: undefined,
denomsSel: denomSelectionInfoToState(selectedDenoms), denomsSel: denomSelectionInfoToState(selectedDenoms),
@ -122,10 +126,10 @@ export async function prepareTip(
const tipStatus: PrepareTipResult = { const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
amount: Amounts.stringify(tipPickupStatus.tip_amount), tipAmountRaw: Amounts.stringify(tipPickupStatus.tip_amount),
exchangeBaseUrl: tipPickupStatus.exchange_url, exchangeBaseUrl: tipPickupStatus.exchange_url,
expirationTimestamp: tipPickupStatus.expiration, expirationTimestamp: tipPickupStatus.expiration,
totalFees: Amounts.stringify(tipRecord.totalFees), tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
walletTipId: tipRecord.walletTipId, walletTipId: tipRecord.walletTipId,
}; };
@ -182,13 +186,13 @@ async function resetTipRetry(
async function processTipImpl( async function processTipImpl(
ws: InternalWalletState, ws: InternalWalletState,
tipId: string, walletTipId: string,
forceNow: boolean, forceNow: boolean,
): Promise<void> { ): Promise<void> {
if (forceNow) { if (forceNow) {
await resetTipRetry(ws, tipId); await resetTipRetry(ws, walletTipId);
} }
let tipRecord = await ws.db.get(Stores.tips, tipId); let tipRecord = await ws.db.get(Stores.tips, walletTipId);
if (!tipRecord) { if (!tipRecord) {
return; return;
} }
@ -216,7 +220,7 @@ async function processTipImpl(
planchets.push(r); planchets.push(r);
} }
} }
await ws.db.mutate(Stores.tips, tipId, (r) => { await ws.db.mutate(Stores.tips, walletTipId, (r) => {
if (!r.planchets) { if (!r.planchets) {
r.planchets = planchets; r.planchets = planchets;
} }
@ -224,7 +228,7 @@ async function processTipImpl(
}); });
} }
tipRecord = await ws.db.get(Stores.tips, tipId); tipRecord = await ws.db.get(Stores.tips, walletTipId);
checkDbInvariant(!!tipRecord, "tip record should be in database"); checkDbInvariant(!!tipRecord, "tip record should be in database");
checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets"); checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets");
@ -246,55 +250,68 @@ async function processTipImpl(
codecForTipResponse(), codecForTipResponse(),
); );
if (response.reserve_sigs.length !== tipRecord.planchets.length) { if (response.blind_sigs.length !== tipRecord.planchets.length) {
throw Error("number of tip responses does not match requested planchets"); throw Error("number of tip responses does not match requested planchets");
} }
const withdrawalGroupId = encodeCrock(getRandomBytes(32)); const newCoinRecords: CoinRecord[] = [];
const planchets: PlanchetRecord[] = [];
for (let i = 0; i < tipRecord.planchets.length; i++) { for (let i = 0; i < response.blind_sigs.length; i++) {
const tipPlanchet = tipRecord.planchets[i]; const blindedSig = response.blind_sigs[i].blind_sig;
const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
const planchet: PlanchetRecord = { const planchet = tipRecord.planchets[i];
blindingKey: tipPlanchet.blindingKey,
coinEv: tipPlanchet.coinEv, const denomSig = await ws.cryptoApi.rsaUnblind(
coinPriv: tipPlanchet.coinPriv, blindedSig,
coinPub: tipPlanchet.coinPub, planchet.blindingKey,
coinValue: tipPlanchet.coinValue, planchet.denomPub,
denomPub: tipPlanchet.denomPub, );
denomPubHash: tipPlanchet.denomPubHash,
reservePub: response.reserve_pub, const isValid = await ws.cryptoApi.rsaVerify(
withdrawSig: response.reserve_sigs[i].reserve_sig, planchet.coinPub,
isFromTip: true, denomSig,
coinEvHash, planchet.denomPub,
coinIdx: i, );
withdrawalDone: false,
withdrawalGroupId: withdrawalGroupId, if (!isValid) {
lastError: undefined, await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
}; const tipRecord = await tx.get(Stores.tips, walletTipId);
planchets.push(planchet); if (!tipRecord) {
return;
}
tipRecord.lastError = makeErrorDetails(
TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
"invalid signature from the exchange (via merchant tip) after unblinding",
{},
);
await tx.put(Stores.tips, tipRecord);
});
return;
}
newCoinRecords.push({
blindingKey: planchet.blindingKey,
coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub,
coinSource: {
type: CoinSourceType.Tip,
coinIndex: i,
walletTipId: walletTipId,
},
currentAmount: planchet.coinValue,
denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash,
denomSig: denomSig,
exchangeBaseUrl: tipRecord.exchangeUrl,
status: CoinStatus.Fresh,
suspended: false,
});
} }
const withdrawalGroup: WithdrawalGroupRecord = {
exchangeBaseUrl: tipRecord.exchangeUrl,
source: {
type: WithdrawalSourceType.Tip,
tipId: tipRecord.walletTipId,
},
timestampStart: getTimestampNow(),
withdrawalGroupId: withdrawalGroupId,
rawWithdrawalAmount: tipRecord.amount,
retryInfo: initRetryInfo(),
timestampFinish: undefined,
lastError: undefined,
denomsSel: tipRecord.denomsSel,
};
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.tips, Stores.withdrawalGroups], [Stores.coins, Stores.tips, Stores.withdrawalGroups],
async (tx) => { async (tx) => {
const tr = await tx.get(Stores.tips, tipId); const tr = await tx.get(Stores.tips, walletTipId);
if (!tr) { if (!tr) {
return; return;
} }
@ -303,16 +320,12 @@ async function processTipImpl(
} }
tr.pickedUp = true; tr.pickedUp = true;
tr.retryInfo = initRetryInfo(false); tr.retryInfo = initRetryInfo(false);
await tx.put(Stores.tips, tr); await tx.put(Stores.tips, tr);
await tx.put(Stores.withdrawalGroups, withdrawalGroup); for (const cr of newCoinRecords) {
for (const p of planchets) { await tx.put(Stores.coins, cr);
await tx.put(Stores.planchets, p);
} }
}, },
); );
await processWithdrawGroup(ws, withdrawalGroupId);
} }
export async function acceptTip( export async function acceptTip(

View File

@ -116,63 +116,49 @@ export async function getTransactions(
return; return;
} }
switch (wsr.source.type) { const r = await tx.get(Stores.reserves, wsr.reservePub);
case WithdrawalSourceType.Reserve: if (!r) {
{ return;
const r = await tx.get(Stores.reserves, wsr.source.reservePub);
if (!r) {
break;
}
let amountRaw: AmountJson | undefined = undefined;
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
amountRaw = r.instructedAmount;
} else {
amountRaw = wsr.denomsSel.totalWithdrawCost;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
const exchange = await tx.get(
Stores.exchanges,
r.exchangeBaseUrl,
);
if (!exchange) {
// FIXME: report somehow
break;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris:
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(
wsr.denomsSel.totalCoinValue,
),
amountRaw: Amounts.stringify(amountRaw),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
...(wsr.lastError ? { error: wsr.lastError } : {}),
});
}
break;
default:
// Tips are reported via their own event
break;
} }
let amountRaw: AmountJson | undefined = undefined;
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
amountRaw = r.instructedAmount;
} else {
amountRaw = wsr.denomsSel.totalWithdrawCost;
}
let withdrawalDetails: WithdrawalDetails;
if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
} else {
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
if (!exchange) {
// FIXME: report somehow
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris:
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
};
}
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(amountRaw),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
...(wsr.lastError ? { error: wsr.lastError } : {}),
});
}); });
// Report pending withdrawals based on reserves that // Report pending withdrawals based on reserves that

View File

@ -242,12 +242,9 @@ async function processPlanchetGenerate(
if (!denom) { if (!denom) {
throw Error("invariant violated"); throw Error("invariant violated");
} }
if (withdrawalGroup.source.type != WithdrawalSourceType.Reserve) {
throw Error("invariant violated");
}
const reserve = await ws.db.get( const reserve = await ws.db.get(
Stores.reserves, Stores.reserves,
withdrawalGroup.source.reservePub, withdrawalGroup.reservePub,
); );
if (!reserve) { if (!reserve) {
throw Error("invariant violated"); throw Error("invariant violated");
@ -420,7 +417,7 @@ async function processPlanchetVerifyAndStoreCoin(
if (!isValid) { if (!isValid) {
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [ let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
withdrawalGroupId, withdrawalGroupId,
coinIdx, coinIdx,
]); ]);
@ -700,7 +697,7 @@ async function processWithdrawGroupImpl(
if (finishedForFirstTime) { if (finishedForFirstTime) {
ws.notify({ ws.notify({
type: NotificationType.WithdrawGroupFinished, type: NotificationType.WithdrawGroupFinished,
withdrawalSource: withdrawalGroup.source, reservePub: withdrawalGroup.reservePub,
}); });
} }
} }

View File

@ -694,17 +694,28 @@ export interface PlanchetRecord {
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
/** /**
* Public key of the reserve, this might be a reserve not * Public key of the reserve that this planchet
* known to the wallet if the planchet is from a tip. * is being withdrawn from.
*
* Can be the empty string (non-null/undefined for DB indexing)
* if this is a tipping reserve.
*/ */
reservePub: string; reservePub: string;
denomPubHash: string; denomPubHash: string;
denomPub: string; denomPub: string;
blindingKey: string; blindingKey: string;
withdrawSig: string; withdrawSig: string;
coinEv: string; coinEv: string;
coinEvHash: string; coinEvHash: string;
coinValue: AmountJson; coinValue: AmountJson;
isFromTip: boolean; isFromTip: boolean;
} }
@ -772,6 +783,8 @@ export interface RefreshCoinSource {
export interface TipCoinSource { export interface TipCoinSource {
type: CoinSourceType.Tip; type: CoinSourceType.Tip;
walletTipId: string;
coinIndex: number;
} }
export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
@ -950,9 +963,9 @@ export interface TipRecord {
/** /**
* The tipped amount. * The tipped amount.
*/ */
amount: AmountJson; tipAmountRaw: AmountJson;
totalFees: AmountJson; tipAmountEffective: AmountJson;
/** /**
* Timestamp, the tip can't be picked up anymore after this deadline. * Timestamp, the tip can't be picked up anymore after this deadline.
@ -1481,18 +1494,6 @@ export enum WithdrawalSourceType {
Reserve = "reserve", Reserve = "reserve",
} }
export interface WithdrawalSourceTip {
type: WithdrawalSourceType.Tip;
tipId: string;
}
export interface WithdrawalSourceReserve {
type: WithdrawalSourceType.Reserve;
reservePub: string;
}
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
export interface DenominationSelectionInfo { export interface DenominationSelectionInfo {
totalCoinValue: AmountJson; totalCoinValue: AmountJson;
totalWithdrawCost: AmountJson; totalWithdrawCost: AmountJson;
@ -1524,12 +1525,7 @@ export interface DenomSelectionState {
export interface WithdrawalGroupRecord { export interface WithdrawalGroupRecord {
withdrawalGroupId: string; withdrawalGroupId: string;
/** reservePub: string;
* Withdrawal source. Fields that don't apply to the respective
* withdrawal source type must be null (i.e. can't be absent),
* otherwise the IndexedDB indexing won't like us.
*/
source: WithdrawalSource;
exchangeBaseUrl: string; exchangeBaseUrl: string;

View File

@ -23,7 +23,6 @@
* Imports. * Imports.
*/ */
import { TalerErrorDetails } from "./walletTypes"; import { TalerErrorDetails } from "./walletTypes";
import { WithdrawalSource } from "./dbTypes";
import { ReserveHistorySummary } from "../util/reserveHistoryUtil"; import { ReserveHistorySummary } from "../util/reserveHistoryUtil";
export enum NotificationType { export enum NotificationType {
@ -141,7 +140,7 @@ export interface WithdrawalGroupCreatedNotification {
export interface WithdrawalGroupFinishedNotification { export interface WithdrawalGroupFinishedNotification {
type: NotificationType.WithdrawGroupFinished; type: NotificationType.WithdrawGroupFinished;
withdrawalSource: WithdrawalSource; reservePub: string;
} }
export interface WaitingForRetryNotification { export interface WaitingForRetryNotification {

View File

@ -22,7 +22,7 @@
* Imports. * Imports.
*/ */
import { TalerErrorDetails, BalancesResponse } from "./walletTypes"; import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes"; import { RetryInfo, ReserveRecordStatus } from "./dbTypes";
import { Timestamp, Duration } from "../util/time"; import { Timestamp, Duration } from "../util/time";
export enum PendingOperationType { export enum PendingOperationType {
@ -219,7 +219,6 @@ export interface PendingRecoupOperation {
*/ */
export interface PendingWithdrawOperation { export interface PendingWithdrawOperation {
type: PendingOperationType.Withdraw; type: PendingOperationType.Withdraw;
source: WithdrawalSource;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
retryInfo: RetryInfo; retryInfo: RetryInfo;
withdrawalGroupId: string; withdrawalGroupId: string;

View File

@ -593,11 +593,11 @@ export interface TipPickupRequest {
* Reserve signature, defined as separate class to facilitate * Reserve signature, defined as separate class to facilitate
* schema validation with "@Checkable". * schema validation with "@Checkable".
*/ */
export class ReserveSigSingleton { export class BlindSigWrapper {
/** /**
* Reserve signature. * Reserve signature.
*/ */
reserve_sig: string; blind_sig: string;
} }
/** /**
@ -605,15 +605,10 @@ export class ReserveSigSingleton {
* to the TipPickupRequest. * to the TipPickupRequest.
*/ */
export class TipResponse { export class TipResponse {
/**
* Public key of the reserve
*/
reserve_pub: string;
/** /**
* The order of the signatures matches the planchets list. * The order of the signatures matches the planchets list.
*/ */
reserve_sigs: ReserveSigSingleton[]; blind_sigs: BlindSigWrapper[];
} }
/** /**
@ -1166,15 +1161,14 @@ export const codecForMerchantRefundResponse = (): Codec<
.property("refunds", codecForList(codecForMerchantRefundPermission())) .property("refunds", codecForList(codecForMerchantRefundPermission()))
.build("MerchantRefundResponse"); .build("MerchantRefundResponse");
export const codecForReserveSigSingleton = (): Codec<ReserveSigSingleton> => export const codecForBlindSigWrapper = (): Codec<BlindSigWrapper> =>
buildCodecForObject<ReserveSigSingleton>() buildCodecForObject<BlindSigWrapper>()
.property("reserve_sig", codecForString()) .property("blind_sig", codecForString())
.build("ReserveSigSingleton"); .build("BlindSigWrapper");
export const codecForTipResponse = (): Codec<TipResponse> => export const codecForTipResponse = (): Codec<TipResponse> =>
buildCodecForObject<TipResponse>() buildCodecForObject<TipResponse>()
.property("reserve_pub", codecForString()) .property("blind_sigs", codecForList(codecForBlindSigWrapper()))
.property("reserve_sigs", codecForList(codecForReserveSigSingleton()))
.build("TipResponse"); .build("TipResponse");
export const codecForRecoup = (): Codec<Recoup> => export const codecForRecoup = (): Codec<Recoup> =>

View File

@ -359,8 +359,8 @@ export interface PrepareTipResult {
* Has the tip already been accepted? * Has the tip already been accepted?
*/ */
accepted: boolean; accepted: boolean;
amount: AmountString; tipAmountRaw: AmountString;
totalFees: AmountString; tipAmountEffective: AmountString;
exchangeBaseUrl: string; exchangeBaseUrl: string;
expirationTimestamp: Timestamp; expirationTimestamp: Timestamp;
} }
@ -368,8 +368,8 @@ export interface PrepareTipResult {
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> => export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
buildCodecForObject<PrepareTipResult>() buildCodecForObject<PrepareTipResult>()
.property("accepted", codecForBoolean()) .property("accepted", codecForBoolean())
.property("amount", codecForAmountString()) .property("tipAmountRaw", codecForAmountString())
.property("totalFees", codecForAmountString()) .property("tipAmountEffective", codecForAmountString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("expirationTimestamp", codecForTimestamp) .property("expirationTimestamp", codecForTimestamp)
.property("walletTipId", codecForString()) .property("walletTipId", codecForString())

View File

@ -86,7 +86,6 @@ import {
codecForPreparePayRequest, codecForPreparePayRequest,
codecForIntegrationTestArgs, codecForIntegrationTestArgs,
WithdrawTestBalanceRequest, WithdrawTestBalanceRequest,
withdrawTestBalanceDefaults,
codecForWithdrawTestBalance, codecForWithdrawTestBalance,
codecForTestPayArgs, codecForTestPayArgs,
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
@ -916,9 +915,7 @@ export class Wallet {
console.error("no withdrawal session found for coin"); console.error("no withdrawal session found for coin");
continue; continue;
} }
if (ws.source.type == "reserve") { withdrawalReservePub = ws.reservePub;
withdrawalReservePub = ws.source.reservePub;
}
} }
coinsJson.coins.push({ coinsJson.coins.push({
coin_pub: c.coinPub, coin_pub: c.coinPub,