diff options
-rw-r--r-- | src/crypto/cryptoApi.ts | 147 | ||||
-rw-r--r-- | src/crypto/cryptoWorker.ts | 299 | ||||
-rw-r--r-- | src/crypto/emscInterface.ts | 32 | ||||
-rw-r--r-- | src/wallet.ts | 1436 |
4 files changed, 1218 insertions, 696 deletions
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index 03c2a675b..43a3bc228 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -14,7 +14,6 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** * API to access the Taler crypto worker thread. * @author Florian Dold @@ -35,22 +34,14 @@ import { WireFee, } from "../dbTypes"; -import { - ContractTerms, - PaybackRequest, -} from "../talerTypes"; +import { ContractTerms, PaybackRequest } from "../talerTypes"; -import { - BenchmarkResult, - CoinWithDenom, - PayCoinInfo, -} from "../walletTypes"; +import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes"; import * as timer from "../timer"; import { startWorker } from "./startWorker"; - /** * State of a crypto worker. */ @@ -58,17 +49,17 @@ interface WorkerState { /** * The actual worker thread. */ - w: Worker|null; + w: Worker | null; /** * Work we're currently executing or null if not busy. */ - currentWorkItem: WorkItem|null; + currentWorkItem: WorkItem | null; /** * Timer to terminate the worker if it's not busy enough. */ - terminationTimerHandle: timer.TimerHandle|null; + terminationTimerHandle: timer.TimerHandle | null; } interface WorkItem { @@ -88,7 +79,6 @@ interface WorkItem { startTime: number; } - /** * Number of different priorities. Each priority p * must be 0 <= p < NUM_PRIO. @@ -151,8 +141,10 @@ export class CryptoApi { handleWorkerError(ws: WorkerState, e: ErrorEvent) { if (ws.currentWorkItem) { - console.error(`error in worker during ${ws.currentWorkItem!.operation}`, - e); + console.error( + `error in worker during ${ws.currentWorkItem!.operation}`, + e, + ); } else { console.error("error in worker", e); } @@ -201,7 +193,10 @@ export class CryptoApi { console.error(`RPC with id ${id} has no registry entry`); return; } - console.log(`rpc ${currentWorkItem.operation} took ${timer.performanceNow() - currentWorkItem.startTime}ms`); + console.log( + `rpc ${currentWorkItem.operation} took ${timer.performanceNow() - + currentWorkItem.startTime}ms`, + ); currentWorkItem.resolve(msg.data.result); } @@ -230,12 +225,21 @@ export class CryptoApi { } } - private doRpc<T>(operation: string, priority: number, - ...args: any[]): Promise<T> { - + private doRpc<T>( + operation: string, + priority: number, + ...args: any[] + ): Promise<T> { const p: Promise<T> = new Promise<T>((resolve, reject) => { const rpcId = this.nextRpcId++; - const workItem: WorkItem = {operation, args, resolve, reject, rpcId, startTime: 0}; + const workItem: WorkItem = { + operation, + args, + resolve, + reject, + rpcId, + startTime: 0, + }; if (this.numBusy === this.workers.length) { const q = this.workQueues[priority]; @@ -263,8 +267,10 @@ export class CryptoApi { }); } - - createPreCoin(denom: DenominationRecord, reserve: ReserveRecord): Promise<PreCoinRecord> { + createPreCoin( + denom: DenominationRecord, + reserve: ReserveRecord, + ): Promise<PreCoinRecord> { return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve); } @@ -280,27 +286,48 @@ export class CryptoApi { return this.doRpc<string>("hashDenomPub", 1, denomPub); } - isValidDenom(denom: DenominationRecord, - masterPub: string): Promise<boolean> { + isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> { return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); } - isValidWireFee(type: string, wf: WireFee, masterPub: string): Promise<boolean> { + isValidWireFee( + type: string, + wf: WireFee, + masterPub: string, + ): Promise<boolean> { return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub); } - isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): Promise<boolean> { - return this.doRpc<boolean>("isValidPaymentSignature", 1, sig, contractHash, merchantPub); + isValidPaymentSignature( + sig: string, + contractHash: string, + merchantPub: string, + ): Promise<boolean> { + return this.doRpc<boolean>( + "isValidPaymentSignature", + 1, + sig, + contractHash, + merchantPub, + ); } - signDeposit(contractTerms: ContractTerms, - cds: CoinWithDenom[], - totalAmount: AmountJson): Promise<PayCoinInfo> { - return this.doRpc<PayCoinInfo>("signDeposit", 3, contractTerms, cds, totalAmount); + signDeposit( + contractTerms: ContractTerms, + cds: CoinWithDenom[], + totalAmount: AmountJson, + ): Promise<PayCoinInfo> { + return this.doRpc<PayCoinInfo>( + "signDeposit", + 3, + contractTerms, + cds, + totalAmount, + ); } - createEddsaKeypair(): Promise<{priv: string, pub: string}> { - return this.doRpc<{priv: string, pub: string}>("createEddsaKeypair", 1); + createEddsaKeypair(): Promise<{ priv: string; pub: string }> { + return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); } rsaUnblind(sig: string, bk: string, pk: string): Promise<string> { @@ -311,23 +338,43 @@ export class CryptoApi { return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin); } - createRefreshSession(exchangeBaseUrl: string, - kappa: number, - meltCoin: CoinRecord, - newCoinDenoms: DenominationRecord[], - meltFee: AmountJson): Promise<RefreshSessionRecord> { - return this.doRpc<RefreshSessionRecord>("createRefreshSession", - 4, - exchangeBaseUrl, - kappa, - meltCoin, - newCoinDenoms, - meltFee); + createRefreshSession( + exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson, + ): Promise<RefreshSessionRecord> { + return this.doRpc<RefreshSessionRecord>( + "createRefreshSession", + 4, + exchangeBaseUrl, + kappa, + meltCoin, + newCoinDenoms, + meltFee, + ); + } + + signCoinLink( + oldCoinPriv: string, + newDenomHash: string, + oldCoinPub: string, + transferPub: string, + coinEv: string, + ): Promise<string> { + return this.doRpc<string>( + "signCoinLink", + 4, + oldCoinPriv, + newDenomHash, + oldCoinPub, + transferPub, + coinEv, + ); } benchmark(repetitions: number): Promise<BenchmarkResult> { - return this.doRpc<BenchmarkResult>("benchmark", - 1, - repetitions); + return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions); } } diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 5013e3acf..9c5263a6f 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -18,7 +18,6 @@ * Web worker for crypto operations. */ - /** * Imports. */ @@ -39,17 +38,9 @@ import { WireFee, } from "../dbTypes"; -import { - CoinPaySig, - ContractTerms, - PaybackRequest, -} from "../talerTypes"; +import { CoinPaySig, ContractTerms, PaybackRequest } from "../talerTypes"; -import { - BenchmarkResult, - CoinWithDenom, - PayCoinInfo, -} from "../walletTypes"; +import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes"; import { canonicalJson } from "../helpers"; @@ -64,15 +55,15 @@ import { } from "./emscInterface"; import * as native from "./emscInterface"; - namespace RpcFunctions { - /** * Create a pre-coin of the given denomination to be withdrawn from then given * reserve. */ - export function createPreCoin(denom: DenominationRecord, - reserve: ReserveRecord): PreCoinRecord { + export function createPreCoin( + denom: DenominationRecord, + reserve: ReserveRecord, + ): PreCoinRecord { const reservePriv = new native.EddsaPrivateKey(); reservePriv.loadCrock(reserve.reserve_priv); const reservePub = new native.EddsaPublicKey(); @@ -125,7 +116,6 @@ namespace RpcFunctions { return preCoin; } - /** * Create a planchet used for tipping, including the private keys. */ @@ -152,12 +142,14 @@ namespace RpcFunctions { coinPub: coinPub.toCrock(), coinValue: denom.value, denomPub: denomPub.encode().toCrock(), - denomPubHash: denomPub.encode().hash().toCrock(), + denomPubHash: denomPub + .encode() + .hash() + .toCrock(), }; return tipPlanchet; } - /** * Create and sign a message to request payback for a coin. */ @@ -165,7 +157,9 @@ namespace RpcFunctions { const p = new native.PaybackRequestPS({ coin_blind: native.RsaBlindingKeySecret.fromCrock(coin.blindingKey), coin_pub: native.EddsaPublicKey.fromCrock(coin.coinPub), - h_denom_pub: native.RsaPublicKey.fromCrock(coin.denomPub).encode().hash(), + h_denom_pub: native.RsaPublicKey.fromCrock(coin.denomPub) + .encode() + .hash(), }); const coinPriv = native.EddsaPrivateKey.fromCrock(coin.coinPriv); const coinSig = native.eddsaSign(p.toPurpose(), coinPriv); @@ -179,63 +173,83 @@ namespace RpcFunctions { return paybackRequest; } - /** * Check if a payment signature is valid. */ - export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): boolean { + export function isValidPaymentSignature( + sig: string, + contractHash: string, + merchantPub: string, + ): boolean { const p = new native.PaymentSignaturePS({ contract_hash: native.HashCode.fromCrock(contractHash), }); const nativeSig = new native.EddsaSignature(); nativeSig.loadCrock(sig); const nativePub = native.EddsaPublicKey.fromCrock(merchantPub); - return native.eddsaVerify(native.SignaturePurpose.MERCHANT_PAYMENT_OK, - p.toPurpose(), - nativeSig, - nativePub); + return native.eddsaVerify( + native.SignaturePurpose.MERCHANT_PAYMENT_OK, + p.toPurpose(), + nativeSig, + nativePub, + ); } /** * Check if a wire fee is correctly signed. */ - export function isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { + export function isValidWireFee( + type: string, + wf: WireFee, + masterPub: string, + ): boolean { const p = new native.MasterWireFeePS({ - closing_fee: (new native.Amount(wf.closingFee)).toNbo(), + closing_fee: new native.Amount(wf.closingFee).toNbo(), end_date: native.AbsoluteTimeNbo.fromStampSeconds(wf.endStamp), h_wire_method: native.ByteArray.fromStringWithNull(type).hash(), start_date: native.AbsoluteTimeNbo.fromStampSeconds(wf.startStamp), - wire_fee: (new native.Amount(wf.wireFee)).toNbo(), + wire_fee: new native.Amount(wf.wireFee).toNbo(), }); const nativeSig = new native.EddsaSignature(); nativeSig.loadCrock(wf.sig); const nativePub = native.EddsaPublicKey.fromCrock(masterPub); - return native.eddsaVerify(native.SignaturePurpose.MASTER_WIRE_FEES, - p.toPurpose(), - nativeSig, - nativePub); + return native.eddsaVerify( + native.SignaturePurpose.MASTER_WIRE_FEES, + p.toPurpose(), + nativeSig, + nativePub, + ); } - /** * Check if the signature of a denomination is valid. */ - export function isValidDenom(denom: DenominationRecord, - masterPub: string): boolean { + export function isValidDenom( + denom: DenominationRecord, + masterPub: string, + ): boolean { const p = new native.DenominationKeyValidityPS({ - denom_hash: native.RsaPublicKey.fromCrock(denom.denomPub) .encode() .hash(), - expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireLegal), - expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireDeposit), - expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireWithdraw), - fee_deposit: (new native.Amount(denom.feeDeposit)).toNbo(), - fee_refresh: (new native.Amount(denom.feeRefresh)).toNbo(), - fee_refund: (new native.Amount(denom.feeRefund)).toNbo(), - fee_withdraw: (new native.Amount(denom.feeWithdraw)).toNbo(), + denom_hash: native.RsaPublicKey.fromCrock(denom.denomPub) + .encode() + .hash(), + expire_legal: native.AbsoluteTimeNbo.fromTalerString( + denom.stampExpireLegal, + ), + expire_spend: native.AbsoluteTimeNbo.fromTalerString( + denom.stampExpireDeposit, + ), + expire_withdraw: native.AbsoluteTimeNbo.fromTalerString( + denom.stampExpireWithdraw, + ), + fee_deposit: new native.Amount(denom.feeDeposit).toNbo(), + fee_refresh: new native.Amount(denom.feeRefresh).toNbo(), + fee_refund: new native.Amount(denom.feeRefund).toNbo(), + fee_withdraw: new native.Amount(denom.feeWithdraw).toNbo(), master: native.EddsaPublicKey.fromCrock(masterPub), start: native.AbsoluteTimeNbo.fromTalerString(denom.stampStart), - value: (new native.Amount(denom.value)).toNbo(), + value: new native.Amount(denom.value).toNbo(), }); const nativeSig = new native.EddsaSignature(); @@ -243,42 +257,44 @@ namespace RpcFunctions { const nativePub = native.EddsaPublicKey.fromCrock(masterPub); - return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, - p.toPurpose(), - nativeSig, - nativePub); - + return native.eddsaVerify( + native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, + p.toPurpose(), + nativeSig, + nativePub, + ); } - /** * Create a new EdDSA key pair. */ - export function createEddsaKeypair(): {priv: string, pub: string} { + export function createEddsaKeypair(): { priv: string; pub: string } { const priv = native.EddsaPrivateKey.create(); const pub = priv.getPublicKey(); - return {priv: priv.toCrock(), pub: pub.toCrock()}; + return { priv: priv.toCrock(), pub: pub.toCrock() }; } - /** * Unblind a blindly signed value. */ export function rsaUnblind(sig: string, bk: string, pk: string): string { - const denomSig = native.rsaUnblind(native.RsaSignature.fromCrock(sig), - native.RsaBlindingKeySecret.fromCrock(bk), - native.RsaPublicKey.fromCrock(pk)); + const denomSig = native.rsaUnblind( + native.RsaSignature.fromCrock(sig), + native.RsaBlindingKeySecret.fromCrock(bk), + native.RsaPublicKey.fromCrock(pk), + ); return denomSig.encode().toCrock(); } - /** * Generate updated coins (to store in the database) * and deposit permissions for each given coin. */ - export function signDeposit(contractTerms: ContractTerms, - cds: CoinWithDenom[], - totalAmount: AmountJson): PayCoinInfo { + export function signDeposit( + contractTerms: ContractTerms, + cds: CoinWithDenom[], + totalAmount: AmountJson, + ): PayCoinInfo { const ret: PayCoinInfo = { originalCoins: [], sigs: [], @@ -287,17 +303,21 @@ namespace RpcFunctions { const contractTermsHash = hashString(canonicalJson(contractTerms)); - const feeList: AmountJson[] = cds.map((x) => x.denom.feeDeposit); - let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount; + const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit); + let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList) + .amount; // okay if saturates - fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)).amount; + fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)) + .amount; const total = Amounts.add(fees, totalAmount).amount; - const amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); + const amountSpent = native.Amount.getZero( + cds[0].coin.currentAmount.currency, + ); const amountRemaining = new native.Amount(total); for (const cd of cds) { let coinSpend: Amount; - const originalCoin = { ...(cd.coin) }; + const originalCoin = { ...cd.coin }; if (amountRemaining.value === 0 && amountRemaining.fraction === 0) { break; @@ -332,13 +352,20 @@ namespace RpcFunctions { h_contract: native.HashCode.fromCrock(contractTermsHash), h_wire: native.HashCode.fromCrock(contractTerms.H_wire), merchant: native.EddsaPublicKey.fromCrock(contractTerms.merchant_pub), - refund_deadline: native.AbsoluteTimeNbo.fromTalerString(contractTerms.refund_deadline), - timestamp: native.AbsoluteTimeNbo.fromTalerString(contractTerms.timestamp), + refund_deadline: native.AbsoluteTimeNbo.fromTalerString( + contractTerms.refund_deadline, + ), + timestamp: native.AbsoluteTimeNbo.fromTalerString( + contractTerms.timestamp, + ), }); - const coinSig = native.eddsaSign(d.toPurpose(), - native.EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) - .toCrock(); + const coinSig = native + .eddsaSign( + d.toPurpose(), + native.EddsaPrivateKey.fromCrock(cd.coin.coinPriv), + ) + .toCrock(); const s: CoinPaySig = { coin_pub: cd.coin.coinPub, @@ -355,22 +382,21 @@ namespace RpcFunctions { return ret; } - /** * Create a new refresh session. */ - export function createRefreshSession(exchangeBaseUrl: string, - kappa: number, - meltCoin: CoinRecord, - newCoinDenoms: DenominationRecord[], - meltFee: AmountJson): RefreshSessionRecord { - + export function createRefreshSession( + exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson, + ): RefreshSessionRecord { let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); for (const ncd of newCoinDenoms) { - valueWithFee = Amounts.add(valueWithFee, - ncd.value, - ncd.feeWithdraw).amount; + valueWithFee = Amounts.add(valueWithFee, ncd.value, ncd.feeWithdraw) + .amount; } // melt fee @@ -397,12 +423,11 @@ namespace RpcFunctions { } sessionHc.read(native.EddsaPublicKey.fromCrock(meltCoin.coinPub)); - sessionHc.read((new native.Amount(valueWithFee)).toNbo()); + sessionHc.read(new native.Amount(valueWithFee).toNbo()); for (let i = 0; i < kappa; i++) { const preCoins: RefreshPreCoinRecord[] = []; for (let j = 0; j < newCoinDenoms.length; j++) { - const transferPriv = native.EcdhePrivateKey.fromCrock(transferPrivs[i]); const oldCoinPub = native.EddsaPublicKey.fromCrock(meltCoin.coinPub); const transferSecret = native.ecdhEddsa(transferPriv, oldCoinPub); @@ -413,10 +438,10 @@ namespace RpcFunctions { const coinPub = coinPriv.getPublicKey(); const blindingFactor = fresh.blindingKey; const pubHash: native.HashCode = coinPub.hash(); - const denomPub = native.RsaPublicKey.fromCrock(newCoinDenoms[j].denomPub); - const ev = native.rsaBlind(pubHash, - blindingFactor, - denomPub); + const denomPub = native.RsaPublicKey.fromCrock( + newCoinDenoms[j].denomPub, + ); + const ev = native.rsaBlind(pubHash, blindingFactor, denomPub); if (!ev) { throw Error("couldn't blind (malicious exchange key?)"); } @@ -437,16 +462,18 @@ namespace RpcFunctions { sessionHc.finish(sessionHash); const confirmData = new RefreshMeltCoinAffirmationPS({ - amount_with_fee: (new Amount(valueWithFee)).toNbo(), + amount_with_fee: new Amount(valueWithFee).toNbo(), coin_pub: EddsaPublicKey.fromCrock(meltCoin.coinPub), - melt_fee: (new Amount(meltFee)).toNbo(), + melt_fee: new Amount(meltFee).toNbo(), session_hash: sessionHash, }); - - const confirmSig: string = native.eddsaSign(confirmData.toPurpose(), - native.EddsaPrivateKey.fromCrock( - meltCoin.coinPriv)).toCrock(); + const confirmSig: string = native + .eddsaSign( + confirmData.toPurpose(), + native.EddsaPrivateKey.fromCrock(meltCoin.coinPriv), + ) + .toCrock(); let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency); for (const denom of newCoinDenoms) { @@ -459,8 +486,8 @@ namespace RpcFunctions { finished: false, hash: sessionHash.toCrock(), meltCoinPub: meltCoin.coinPub, - newDenomHashes: newCoinDenoms.map((d) => d.denomPubHash), - newDenoms: newCoinDenoms.map((d) => d.denomPub), + newDenomHashes: newCoinDenoms.map(d => d.denomPubHash), + newDenoms: newCoinDenoms.map(d => d.denomPub), norevealIndex: undefined, preCoinsForGammas, transferPrivs, @@ -484,7 +511,33 @@ namespace RpcFunctions { * Hash a denomination public key. */ export function hashDenomPub(denomPub: string): string { - return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock(); + return native.RsaPublicKey.fromCrock(denomPub) + .encode() + .hash() + .toCrock(); + } + + export function signCoinLink( + oldCoinPriv: string, + newDenomHash: string, + oldCoinPub: string, + transferPub: string, + coinEv: string, + ): string { + const coinEvHash = native.ByteArray.fromCrock(coinEv).hash(); + + const coinLink = new native.CoinLinkSignaturePS({ + coin_envelope_hash: coinEvHash, + h_denom_pub: native.HashCode.fromCrock(newDenomHash), + old_coin_pub: native.EddsaPublicKey.fromCrock(oldCoinPub), + transfer_pub: native.EcdhePublicKey.fromCrock(transferPub), + }); + + const coinPriv = native.EddsaPrivateKey.fromCrock(oldCoinPriv); + + const sig = native.eddsaSign(coinLink.toPurpose(), coinPriv); + + return sig.toCrock(); } export function benchmark(repetitions: number): BenchmarkResult { @@ -500,7 +553,7 @@ namespace RpcFunctions { for (let i = 0; i < repetitions; i++) { ba.randomize(native.RandomQuality.WEAK); const start = timer.performanceNow(); - ba.hash(); + ba.hash(); time_hash_big += timer.performanceNow() - start; } @@ -508,7 +561,7 @@ namespace RpcFunctions { for (let i = 0; i < repetitions; i++) { const start = timer.performanceNow(); const priv: native.EddsaPrivateKey = native.EddsaPrivateKey.create(); - time_eddsa_create += timer.performanceNow() - start; + time_eddsa_create += timer.performanceNow() - start; priv.destroy(); } @@ -541,14 +594,15 @@ namespace RpcFunctions { priv.destroy(); } - let time_eddsa_verify = 0; for (let i = 0; i < repetitions; i++) { const start = timer.performanceNow(); - native.eddsaVerify(native.SignaturePurpose.MERCHANT_PAYMENT_OK, - p, - eddsaSig, - eddsaPub); + native.eddsaVerify( + native.SignaturePurpose.MERCHANT_PAYMENT_OK, + p, + eddsaSig, + eddsaPub, + ); time_eddsa_verify += timer.performanceNow() - start; } @@ -564,11 +618,18 @@ namespace RpcFunctions { time_rsa_2048_blind += timer.performanceNow() - start; } - const blindedMessage2048 = native.rsaBlind(h, blindingSecret2048, rsaPub2048); + const blindedMessage2048 = native.rsaBlind( + h, + blindingSecret2048, + rsaPub2048, + ); if (!blindedMessage2048) { throw Error("should not happen"); } - const rsaBlindSig2048 = native.rsaSignBlinded(rsaPriv2048, blindedMessage2048); + const rsaBlindSig2048 = native.rsaSignBlinded( + rsaPriv2048, + blindedMessage2048, + ); let time_rsa_2048_unblind = 0; for (let i = 0; i < repetitions; i++) { @@ -577,7 +638,11 @@ namespace RpcFunctions { time_rsa_2048_unblind += timer.performanceNow() - start; } - const unblindedSig2048 = native.rsaUnblind(rsaBlindSig2048, blindingSecret2048, rsaPub2048); + const unblindedSig2048 = native.rsaUnblind( + rsaBlindSig2048, + blindingSecret2048, + rsaPub2048, + ); let time_rsa_2048_verify = 0; for (let i = 0; i < repetitions; i++) { @@ -586,7 +651,6 @@ namespace RpcFunctions { time_rsa_2048_verify += timer.performanceNow() - start; } - /* rsa 4096 */ let time_rsa_4096_blind = 0; @@ -599,11 +663,18 @@ namespace RpcFunctions { time_rsa_4096_blind += timer.performanceNow() - start; } - const blindedMessage4096 = native.rsaBlind(h, blindingSecret4096, rsaPub4096); + const blindedMessage4096 = native.rsaBlind( + h, + blindingSecret4096, + rsaPub4096, + ); if (!blindedMessage4096) { throw Error("should not happen"); } - const rsaBlindSig4096 = native.rsaSignBlinded(rsaPriv4096, blindedMessage4096); + const rsaBlindSig4096 = native.rsaSignBlinded( + rsaPriv4096, + blindedMessage4096, + ); let time_rsa_4096_unblind = 0; for (let i = 0; i < repetitions; i++) { @@ -612,7 +683,11 @@ namespace RpcFunctions { time_rsa_4096_unblind += timer.performanceNow() - start; } - const unblindedSig4096 = native.rsaUnblind(rsaBlindSig4096, blindingSecret4096, rsaPub4096); + const unblindedSig4096 = native.rsaUnblind( + rsaBlindSig4096, + blindingSecret4096, + rsaPub4096, + ); let time_rsa_4096_verify = 0; for (let i = 0; i < repetitions; i++) { @@ -621,7 +696,6 @@ namespace RpcFunctions { time_rsa_4096_verify += timer.performanceNow() - start; } - return { repetitions, time: { @@ -637,12 +711,11 @@ namespace RpcFunctions { rsa_4096_blind: time_rsa_4096_blind, rsa_4096_unblind: time_rsa_4096_unblind, rsa_4096_verify: time_rsa_4096_verify, - } + }, }; } } - const worker: Worker = (self as any) as Worker; worker.onmessage = (msg: MessageEvent) => { @@ -665,7 +738,7 @@ worker.onmessage = (msg: MessageEvent) => { console.log("onmessage with", msg.data.operation); console.log("foo"); - emscLoader.getLib().then((p) => { + emscLoader.getLib().then(p => { const lib = p.lib; if (!native.isInitialized()) { console.log("initializing emscripten for then first time with lib"); diff --git a/src/crypto/emscInterface.ts b/src/crypto/emscInterface.ts index dcd16e633..2ddc15a37 100644 --- a/src/crypto/emscInterface.ts +++ b/src/crypto/emscInterface.ts @@ -223,6 +223,7 @@ export enum SignaturePurpose { MERCHANT_PAYMENT_OK = 1104, MASTER_WIRE_FEES = 1028, WALLET_COIN_PAYBACK = 1203, + WALLET_COIN_LINK = 1204, } @@ -970,7 +971,7 @@ abstract class SignatureStruct { throw Error(`Key ${name} not found`); } if (!(value instanceof typemap[name])) { - throw Error("Wrong type for ${name}"); + throw Error(`Wrong type for ${name}`); } this.members[name] = value; } @@ -1293,6 +1294,35 @@ export class DepositRequestPS extends SignatureStruct { } } + +interface CoinLinkSignaturePS_args { + h_denom_pub: HashCode; + old_coin_pub: EddsaPublicKey; + transfer_pub: EcdhePublicKey; + coin_envelope_hash: HashCode; +} + + +export class CoinLinkSignaturePS extends SignatureStruct { + constructor(w: CoinLinkSignaturePS_args) { + super(w); + } + + purpose() { + return SignaturePurpose.WALLET_COIN_LINK; + } + + fieldTypes() { + return [ + ["h_denom_pub", HashCode], + ["old_coin_pub", EddsaPublicKey], + ["transfer_pub", EcdhePublicKey], + ["coin_envelope_hash", HashCode], + ]; + } +} + + /** * Arguments for constuctor of [[DenominationKeyValidityPS]]. */ diff --git a/src/wallet.ts b/src/wallet.ts index 4c7e8c181..0dfb77554 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -30,10 +30,7 @@ import { getTalerStampSec, strcmp, } from "./helpers"; -import { - HttpRequestLibrary, - RequestException, -} from "./http"; +import { HttpRequestLibrary, RequestException } from "./http"; import * as LibtoolVersion from "./libtoolVersion"; import { AbortTransaction, @@ -109,7 +106,6 @@ import { WalletBalanceEntry, } from "./walletTypes"; - interface SpeculativePayData { payCoinInfo: PayCoinInfo; exchangeUrl: string; @@ -117,7 +113,6 @@ interface SpeculativePayData { proposal: ProposalDownloadRecord; } - /** * Wallet protocol version spoken with the exchange * and merchant. @@ -132,7 +127,7 @@ const builtinCurrencies: CurrencyRecord[] = [ { auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0", baseUrl: "https://auditor.demo.taler.net/", - expirationStamp: (new Date(2027, 1)).getTime(), + expirationStamp: new Date(2027, 1).getTime(), }, ], exchanges: [], @@ -141,9 +136,8 @@ const builtinCurrencies: CurrencyRecord[] = [ }, ]; - function isWithdrawableDenom(d: DenominationRecord) { - const nowSec = (new Date()).getTime() / 1000; + const nowSec = new Date().getTime() / 1000; const stampWithdrawSec = getTalerStampSec(d.stampExpireWithdraw); if (stampWithdrawSec === null) { return false; @@ -153,19 +147,17 @@ function isWithdrawableDenom(d: DenominationRecord) { return false; } // Withdraw if still possible to withdraw within a minute - if ((stampWithdrawSec + 60 > nowSec) && (nowSec >= stampStartSec)) { + if (stampWithdrawSec + 60 > nowSec && nowSec >= stampStartSec) { return true; } return false; } - interface SelectPayCoinsResult { cds: CoinWithDenom[]; totalFees: AmountJson; } - /** * Get the amount that we lose when refreshing a coin of the given denomination * with a certain amount left. @@ -177,38 +169,54 @@ interface SelectPayCoinsResult { * Considers refresh fees, withdrawal fees after refresh and amounts too small * to refresh. */ -export function getTotalRefreshCost(denoms: DenominationRecord[], - refreshedDenom: DenominationRecord, - amountLeft: AmountJson): AmountJson { - const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh).amount; +export function getTotalRefreshCost( + denoms: DenominationRecord[], + refreshedDenom: DenominationRecord, + amountLeft: AmountJson, +): AmountJson { + const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh) + .amount; const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms); - const resultingAmount = Amounts.add(Amounts.getZero(withdrawAmount.currency), - ...withdrawDenoms.map((d) => d.value)).amount; + const resultingAmount = Amounts.add( + Amounts.getZero(withdrawAmount.currency), + ...withdrawDenoms.map(d => d.value), + ).amount; const totalCost = Amounts.sub(amountLeft, resultingAmount).amount; - console.log("total refresh cost for", amountToPretty(amountLeft), "is", amountToPretty(totalCost)); + console.log( + "total refresh cost for", + amountToPretty(amountLeft), + "is", + amountToPretty(totalCost), + ); return totalCost; } - /** * Select coins for a payment under the merchant's constraints. * * @param denoms all available denoms, used to compute refresh fees */ -export function selectPayCoins(denoms: DenominationRecord[], cds: CoinWithDenom[], paymentAmount: AmountJson, - depositFeeLimit: AmountJson): SelectPayCoinsResult|undefined { +export function selectPayCoins( + denoms: DenominationRecord[], + cds: CoinWithDenom[], + paymentAmount: AmountJson, + depositFeeLimit: AmountJson, +): SelectPayCoinsResult | undefined { if (cds.length === 0) { return undefined; } // Sort by ascending deposit fee and denomPub if deposit fee is the same // (to guarantee deterministic results) - cds.sort((o1, o2) => Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) || - strcmp(o1.denom.denomPub, o2.denom.denomPub)); + cds.sort( + (o1, o2) => + Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) || + strcmp(o1.denom.denomPub, o2.denom.denomPub), + ); const currency = cds[0].denom.value.currency; const cdsResult: CoinWithDenom[] = []; let accDepositFee: AmountJson = Amounts.getZero(currency); let accAmount: AmountJson = Amounts.getZero(currency); - for (const {coin, denom} of cds) { + for (const { coin, denom } of cds) { if (coin.suspended) { continue; } @@ -218,20 +226,32 @@ export function selectPayCoins(denoms: DenominationRecord[], cds: CoinWithDenom[ if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) { continue; } - cdsResult.push({coin, denom}); + cdsResult.push({ coin, denom }); accDepositFee = Amounts.add(denom.feeDeposit, accDepositFee).amount; - let leftAmount = Amounts.sub(coin.currentAmount, Amounts.sub(paymentAmount, accAmount).amount).amount; + let leftAmount = Amounts.sub( + coin.currentAmount, + Amounts.sub(paymentAmount, accAmount).amount, + ).amount; accAmount = Amounts.add(coin.currentAmount, accAmount).amount; const coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0; - const coversAmountWithFee = Amounts.cmp(accAmount, - Amounts.add(paymentAmount, - denom.feeDeposit).amount) >= 0; + const coversAmountWithFee = + Amounts.cmp( + accAmount, + Amounts.add(paymentAmount, denom.feeDeposit).amount, + ) >= 0; const isBelowFee = Amounts.cmp(accDepositFee, depositFeeLimit) <= 0; - console.log("coin selection", { coversAmount, isBelowFee, accDepositFee, accAmount, paymentAmount }); + console.log("coin selection", { + coversAmount, + isBelowFee, + accDepositFee, + accAmount, + paymentAmount, + }); if ((coversAmount && isBelowFee) || coversAmountWithFee) { - const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit).amount; + const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit) + .amount; leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount; console.log("deposit fee to cover", amountToPretty(depositFeeToCover)); @@ -241,21 +261,25 @@ export function selectPayCoins(denoms: DenominationRecord[], cds: CoinWithDenom[ // because the merchant doesn't cover them totalFees = Amounts.sub(depositFeeLimit, accDepositFee).amount; } - totalFees = Amounts.add(totalFees, getTotalRefreshCost(denoms, denom, leftAmount)).amount; + totalFees = Amounts.add( + totalFees, + getTotalRefreshCost(denoms, denom, leftAmount), + ).amount; return { cds: cdsResult, totalFees }; } } return undefined; } - /** * Get a list of denominations (with repetitions possible) * whose total value is as close as possible to the available * amount, but never larger. */ -function getWithdrawDenomList(amountAvailable: AmountJson, - denoms: DenominationRecord[]): DenominationRecord[] { +function getWithdrawDenomList( + amountAvailable: AmountJson, + denoms: DenominationRecord[], +): DenominationRecord[] { let remaining = Amounts.copy(amountAvailable); const ds: DenominationRecord[] = []; @@ -284,7 +308,6 @@ function getWithdrawDenomList(amountAvailable: AmountJson, return ds; } - interface CoinsForPaymentArgs { allowedAuditors: Auditor[]; allowedExchanges: ExchangeHandle[]; @@ -296,7 +319,6 @@ interface CoinsForPaymentArgs { wireMethod: string; } - /** * The platform-independent wallet implementation. */ @@ -310,7 +332,7 @@ export class Wallet { private notifier: Notifier; private cryptoApi: CryptoApi; private processPreCoinConcurrent = 0; - private processPreCoinThrottle: {[url: string]: number} = {}; + private processPreCoinThrottle: { [url: string]: number } = {}; private timerGroup: TimerGroup; private speculativePayData: SpeculativePayData | undefined; private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {}; @@ -325,10 +347,12 @@ export class Wallet { return new QueryRoot(this.db); } - constructor(db: IDBDatabase, - http: HttpRequestLibrary, - badge: Badge, - notifier: Notifier) { + constructor( + db: IDBDatabase, + http: HttpRequestLibrary, + badge: Badge, + notifier: Notifier, + ) { this.db = db; this.http = http; this.badge = badge; @@ -337,8 +361,8 @@ export class Wallet { this.timerGroup = new TimerGroup(); const init = async () => { - await this.fillDefaults().catch((e) => console.log(e)); - await this.collectGarbage().catch((e) => console.log(e)); + await this.fillDefaults().catch(e => console.log(e)); + await this.collectGarbage().catch(e => console.log(e)); this.updateExchanges(); this.resumePendingFromDb(); this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges()); @@ -353,20 +377,17 @@ export class Wallet { }; const onFalse = (r: QueryRoot) => { console.log("applying defaults"); - r.put(Stores.config, {key: "currencyDefaultsApplied", value: true}) + r.put(Stores.config, { key: "currencyDefaultsApplied", value: true }) .putAll(Stores.currencies, builtinCurrencies) .finish(); }; - await ( - this.q() - .iter(Stores.config) - .filter((x) => x.key === "currencyDefaultsApplied") - .first() - .cond((x) => x && x.value, onTrue, onFalse) - ); + await this.q() + .iter(Stores.config) + .filter(x => x.key === "currencyDefaultsApplied") + .first() + .cond(x => x && x.value, onTrue, onFalse); } - private startOperation(operationId: string) { this.runningOperations.add(operationId); this.badge.startBusy(); @@ -383,15 +404,14 @@ export class Wallet { console.log("updating exchanges"); const exchangesUrls = await this.q() - .iter(Stores.exchanges) - .map((e) => e.baseUrl) - .toArray(); + .iter(Stores.exchanges) + .map(e => e.baseUrl) + .toArray(); for (const url of exchangesUrls) { - this.updateExchangeFromUrl(url) - .catch((e) => { - console.error("updating exchange failed", e); - }); + this.updateExchangeFromUrl(url).catch(e => { + console.error("updating exchange failed", e); + }); } } @@ -403,68 +423,69 @@ export class Wallet { console.log("resuming pending operations from db"); this.q() - .iter(Stores.reserves) - .forEach((reserve) => { - console.log("resuming reserve", reserve.reserve_pub); - this.processReserve(reserve); - }); + .iter(Stores.reserves) + .forEach(reserve => { + console.log("resuming reserve", reserve.reserve_pub); + this.processReserve(reserve); + }); this.q() - .iter(Stores.precoins) - .forEach((preCoin) => { - console.log("resuming precoin"); - this.processPreCoin(preCoin); - }); + .iter(Stores.precoins) + .forEach(preCoin => { + console.log("resuming precoin"); + this.processPreCoin(preCoin); + }); this.q() - .iter(Stores.refresh) - .forEach((r: RefreshSessionRecord) => { - this.continueRefreshSession(r); - }); + .iter(Stores.refresh) + .forEach((r: RefreshSessionRecord) => { + this.continueRefreshSession(r); + }); this.q() - .iter(Stores.coinsReturns) - .forEach((r: CoinsReturnRecord) => { - this.depositReturnedCoins(r); - }); + .iter(Stores.coinsReturns) + .forEach((r: CoinsReturnRecord) => { + this.depositReturnedCoins(r); + }); // FIXME: optimize via index this.q() - .iter(Stores.coins) - .forEach((c: CoinRecord) => { - if (c.status === CoinStatus.Dirty) { - console.log("resuming pending refresh for coin", c); - this.refresh(c.coinPub); - } - }); + .iter(Stores.coins) + .forEach((c: CoinRecord) => { + if (c.status === CoinStatus.Dirty) { + console.log("resuming pending refresh for coin", c); + this.refresh(c.coinPub); + } + }); } - - private async getCoinsForReturn(exchangeBaseUrl: string, amount: AmountJson): Promise<CoinWithDenom[] | undefined> { + private async getCoinsForReturn( + exchangeBaseUrl: string, + amount: AmountJson, + ): Promise<CoinWithDenom[] | undefined> { const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`); } - const coins: CoinRecord[] = await ( - this.q() - .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) - .toArray() - ); + const coins: CoinRecord[] = await this.q() + .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); if (!coins || !coins.length) { return []; } - const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray(); + const denoms = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); // Denomination of the first coin, we assume that all other // coins have the same currency - const firstDenom = await this.q().get(Stores.denominations, - [ - exchange.baseUrl, - coins[0].denomPub, - ]); + const firstDenom = await this.q().get(Stores.denominations, [ + exchange.baseUrl, + coins[0].denomPub, + ]); if (!firstDenom) { throw Error("db inconsistent"); } @@ -472,13 +493,19 @@ export class Wallet { const cds: CoinWithDenom[] = []; for (const coin of coins) { - const denom = await this.q().get(Stores.denominations, - [exchange.baseUrl, coin.denomPub]); + const denom = await this.q().get(Stores.denominations, [ + exchange.baseUrl, + coin.denomPub, + ]); if (!denom) { throw Error("db inconsistent"); } if (denom.value.currency !== currency) { - console.warn(`same pubkey for different currencies at exchange ${exchange.baseUrl}`); + console.warn( + `same pubkey for different currencies at exchange ${ + exchange.baseUrl + }`, + ); continue; } if (coin.suspended) { @@ -487,10 +514,10 @@ export class Wallet { if (coin.status !== CoinStatus.Fresh) { continue; } - cds.push({coin, denom}); + cds.push({ coin, denom }); } - console.log("coin return: selecting from possible coins", { cds, amount } ); + console.log("coin return: selecting from possible coins", { cds, amount }); const res = selectPayCoins(denoms, cds, amount, amount); if (res) { @@ -499,12 +526,13 @@ export class Wallet { return undefined; } - /** * Get exchanges and associated coins that are still spendable, * but only if the sum the coins' remaining value exceeds the payment amount. */ - private async getCoinsForPayment(args: CoinsForPaymentArgs): Promise<CoinSelectionResult|undefined> { + private async getCoinsForPayment( + args: CoinsForPaymentArgs, + ): Promise<CoinSelectionResult | undefined> { const { allowedAuditors, allowedExchanges, @@ -518,7 +546,9 @@ export class Wallet { let remainingAmount = paymentAmount; - const exchanges = await this.q().iter(Stores.exchanges).toArray(); + const exchanges = await this.q() + .iter(Stores.exchanges) + .toArray(); for (const exchange of exchanges) { let isOkay: boolean = false; @@ -551,34 +581,40 @@ export class Wallet { } const coins: CoinRecord[] = await this.q() - .iterIndex(Stores.coins.exchangeBaseUrlIndex, - exchange.baseUrl) - .toArray(); - const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray(); + .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); + const denoms = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); if (!coins || coins.length === 0) { continue; } // Denomination of the first coin, we assume that all other // coins have the same currency - const firstDenom = await this.q().get(Stores.denominations, - [ - exchange.baseUrl, - coins[0].denomPub, - ]); + const firstDenom = await this.q().get(Stores.denominations, [ + exchange.baseUrl, + coins[0].denomPub, + ]); if (!firstDenom) { throw Error("db inconsistent"); } const currency = firstDenom.value.currency; const cds: CoinWithDenom[] = []; for (const coin of coins) { - const denom = await this.q().get(Stores.denominations, - [exchange.baseUrl, coin.denomPub]); + const denom = await this.q().get(Stores.denominations, [ + exchange.baseUrl, + coin.denomPub, + ]); if (!denom) { throw Error("db inconsistent"); } if (denom.value.currency !== currency) { - console.warn(`same pubkey for different currencies at exchange ${exchange.baseUrl}`); + console.warn( + `same pubkey for different currencies at exchange ${ + exchange.baseUrl + }`, + ); continue; } if (coin.suspended) { @@ -587,10 +623,13 @@ export class Wallet { if (coin.status !== CoinStatus.Fresh) { continue; } - cds.push({coin, denom}); + cds.push({ coin, denom }); } - const fees = await this.q().get(Stores.exchangeWireFees, exchange.baseUrl); + const fees = await this.q().get( + Stores.exchangeWireFees, + exchange.baseUrl, + ); if (!fees) { console.error("no fees found for exchange", exchange); continue; @@ -603,8 +642,8 @@ export class Wallet { console.log("payment coins: wireFeeTime", wireFeeTime); let totalFees = Amounts.getZero(currency); - let wireFee: AmountJson|undefined; - for (const fee of (fees.feesForType[wireMethod] || [])) { + let wireFee: AmountJson | undefined; + for (const fee of fees.feesForType[wireMethod] || []) { if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) { wireFee = fee.wireFee; break; @@ -617,7 +656,8 @@ export class Wallet { const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization); if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) { totalFees = Amounts.add(amortizedWireFee, totalFees).amount; - remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount; + remainingAmount = Amounts.add(amortizedWireFee, remainingAmount) + .amount; } } @@ -635,14 +675,15 @@ export class Wallet { return undefined; } - /** * Record all information that is necessary to * pay for a proposal in the wallet's database. */ - private async recordConfirmPay(proposal: ProposalDownloadRecord, - payCoinInfo: PayCoinInfo, - chosenExchange: string): Promise<PurchaseRecord> { + private async recordConfirmPay( + proposal: ProposalDownloadRecord, + payCoinInfo: PayCoinInfo, + chosenExchange: string, + ): Promise<PurchaseRecord> { const payReq: PayReq = { coins: payCoinInfo.sigs, merchant_pub: proposal.contractTerms.merchant_pub, @@ -661,26 +702,28 @@ export class Wallet { payReq, refundsDone: {}, refundsPending: {}, - timestamp: (new Date()).getTime(), + timestamp: new Date().getTime(), timestamp_refund: 0, }; await this.q() - .put(Stores.purchases, t) - .putAll(Stores.coins, payCoinInfo.updatedCoins) - .finish(); + .put(Stores.purchases, t) + .putAll(Stores.coins, payCoinInfo.updatedCoins) + .finish(); this.badge.showNotification(); this.notifier.notify(); return t; } - /** * Download a proposal and store it in the database. * Returns an id for it to retrieve it later. */ async downloadProposal(url: string): Promise<number> { - const oldProposal = await this.q().getIndexed(Stores.proposals.urlIndex, url); + const oldProposal = await this.q().getIndexed( + Stores.proposals.urlIndex, + url, + ); if (oldProposal) { return oldProposal.id!; } @@ -691,7 +734,7 @@ export class Wallet { console.log("downloading contract from '" + urlWithNonce + "'"); let resp; try { - resp = await axios.get(urlWithNonce, { validateStatus: (s) => s === 200 }); + resp = await axios.get(urlWithNonce, { validateStatus: s => s === 200 }); } catch (e) { console.log("contract download failed", e); throw e; @@ -707,7 +750,7 @@ export class Wallet { contractTermsHash, merchantSig: proposal.sig, noncePriv: priv, - timestamp: (new Date()).getTime(), + timestamp: new Date().getTime(), url, }; @@ -719,16 +762,21 @@ export class Wallet { return id; } - async refundFailedPay(proposalId: number) { console.log(`refunding failed payment with proposal id ${proposalId}`); - const proposal: ProposalDownloadRecord|undefined = await this.q().get(Stores.proposals, proposalId); + const proposal: ProposalDownloadRecord | undefined = await this.q().get( + Stores.proposals, + proposalId, + ); if (!proposal) { throw Error(`proposal with id ${proposalId} not found`); } - const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash); + const purchase = await this.q().get( + Stores.purchases, + proposal.contractTermsHash, + ); if (!purchase) { throw Error("purchase not found for proposal"); } @@ -738,8 +786,10 @@ export class Wallet { } } - - async submitPay(contractTermsHash: string, sessionId: string | undefined): Promise<ConfirmPayResult> { + async submitPay( + contractTermsHash: string, + sessionId: string | undefined, + ): Promise<ConfirmPayResult> { const purchase = await this.q().get(Stores.purchases, contractTermsHash); if (!purchase) { throw Error("Purchase not found: " + contractTermsHash); @@ -753,7 +803,7 @@ export class Wallet { try { const config = { headers: { "Content-Type": "application/json;charset=UTF-8" }, - timeout: 5000, /* 5 seconds */ + timeout: 5000 /* 5 seconds */, validateStatus: (s: number) => s === 200, }; resp = await axios.post(purchase.contractTerms.pay_url, payReq, config); @@ -766,8 +816,10 @@ export class Wallet { console.log("got success from pay_url"); const merchantPub = purchase.contractTerms.merchant_pub; - const valid: boolean = await ( - this.cryptoApi.isValidPaymentSignature(merchantResp.sig, contractTermsHash, merchantPub) + const valid: boolean = await this.cryptoApi.isValidPaymentSignature( + merchantResp.sig, + contractTermsHash, + merchantPub, ); if (!valid) { console.error("merchant payment signature invalid"); @@ -795,31 +847,44 @@ export class Wallet { } await this.q() - .putAll(Stores.coins, modifiedCoins) - .put(Stores.purchases, purchase) - .finish(); + .putAll(Stores.coins, modifiedCoins) + .put(Stores.purchases, purchase) + .finish(); for (const c of purchase.payReq.coins) { this.refresh(c.coin_pub); } const nextUrl = fu.href(); - this.cachedNextUrl[purchase.contractTerms.fulfillment_url] = { nextUrl, lastSessionId: sessionId }; + this.cachedNextUrl[purchase.contractTerms.fulfillment_url] = { + nextUrl, + lastSessionId: sessionId, + }; return { nextUrl }; } - /** * Add a contract to the wallet and sign coins, and send them. */ - async confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> { - console.log(`executing confirmPay with proposalId ${proposalId} and sessionId ${sessionId}`); - const proposal: ProposalDownloadRecord|undefined = await this.q().get(Stores.proposals, proposalId); + async confirmPay( + proposalId: number, + sessionId: string | undefined, + ): Promise<ConfirmPayResult> { + console.log( + `executing confirmPay with proposalId ${proposalId} and sessionId ${sessionId}`, + ); + const proposal: ProposalDownloadRecord | undefined = await this.q().get( + Stores.proposals, + proposalId, + ); if (!proposal) { throw Error(`proposal with id ${proposalId} not found`); } - let purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash); + let purchase = await this.q().get( + Stores.purchases, + proposal.contractTermsHash, + ); if (purchase) { return this.submitPay(purchase.contractTermsHash, sessionId); @@ -857,20 +922,33 @@ export class Wallet { const sd = await this.getSpeculativePayData(proposalId); if (!sd) { const { exchangeUrl, cds, totalAmount } = res; - const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds, totalAmount); - purchase = await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl); + const payCoinInfo = await this.cryptoApi.signDeposit( + proposal.contractTerms, + cds, + totalAmount, + ); + purchase = await this.recordConfirmPay( + proposal, + payCoinInfo, + exchangeUrl, + ); } else { - purchase = await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl); + purchase = await this.recordConfirmPay( + sd.proposal, + sd.payCoinInfo, + sd.exchangeUrl, + ); } return this.submitPay(purchase.contractTermsHash, sessionId); } - /** * Get the speculative pay data, but only if coins have not changed in between. */ - async getSpeculativePayData(proposalId: number): Promise<SpeculativePayData | undefined> { + async getSpeculativePayData( + proposalId: number, + ): Promise<SpeculativePayData | undefined> { const sp = this.speculativePayData; if (!sp) { return; @@ -888,14 +966,15 @@ export class Wallet { if (!currentCoin) { return; } - if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) { + if ( + Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0 + ) { return; } } return sp; } - /** * Check if payment for an offer is possible, or if the offer has already * been payed for. @@ -911,7 +990,10 @@ export class Wallet { } // First check if we already payed for it. - const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash); + const purchase = await this.q().get( + Stores.purchases, + proposal.contractTermsHash, + ); if (purchase) { return { status: "paid" }; } @@ -943,9 +1025,17 @@ export class Wallet { } // Only create speculative signature if we don't already have one for this proposal - if ((!this.speculativePayData) || (this.speculativePayData && this.speculativePayData.proposalId !== proposalId)) { + if ( + !this.speculativePayData || + (this.speculativePayData && + this.speculativePayData.proposalId !== proposalId) + ) { const { exchangeUrl, cds, totalAmount } = res; - const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds, totalAmount); + const payCoinInfo = await this.cryptoApi.signDeposit( + proposal.contractTerms, + cds, + totalAmount, + ); this.speculativePayData = { exchangeUrl, payCoinInfo, @@ -957,15 +1047,19 @@ export class Wallet { return { status: "payment-possible", coinSelection: res }; } - /** * Retrieve information required to pay for a contract, where the * contract is identified via the fulfillment url. */ - async queryPaymentByFulfillmentUrl(url: string): Promise<PurchaseRecord | undefined> { + async queryPaymentByFulfillmentUrl( + url: string, + ): Promise<PurchaseRecord | undefined> { console.log("query for payment", url); - const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url); + const t = await this.q().getIndexed( + Stores.purchases.fulfillmentUrlIndex, + url, + ); if (!t) { console.log("query for payment failed"); @@ -975,13 +1069,14 @@ export class Wallet { return t; } - /** * First fetch information requred to withdraw from the reserve, * then deplete the reserve, withdrawing coins until it is empty. */ - private async processReserve(reserveRecord: ReserveRecord, - retryDelayMs: number = 250): Promise<void> { + private async processReserve( + reserveRecord: ReserveRecord, + retryDelayMs: number = 250, + ): Promise<void> { const opId = "reserve-" + reserveRecord.reserve_pub; this.startOperation(opId); @@ -990,38 +1085,54 @@ export class Wallet { await this.depleteReserve(reserve); } catch (e) { // random, exponential backoff truncated at 3 minutes - const nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), 3000 * 60); - console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`); - this.timerGroup.after(retryDelayMs, () => this.processReserve(reserveRecord, nextDelay)); + const nextDelay = Math.min( + 2 * retryDelayMs + retryDelayMs * Math.random(), + 3000 * 60, + ); + console.warn( + `Failed to deplete reserve, trying again in ${retryDelayMs} ms`, + ); + this.timerGroup.after(retryDelayMs, () => + this.processReserve(reserveRecord, nextDelay), + ); } finally { this.stopOperation(opId); } } - /** * Given a planchet, withdraw a coin from the exchange. */ - private async processPreCoin(preCoin: PreCoinRecord, - retryDelayMs = 200): Promise<void> { + private async processPreCoin( + preCoin: PreCoinRecord, + retryDelayMs = 200, + ): Promise<void> { // Throttle concurrent executions of this function, so we don't withdraw too many coins at once. - if (this.processPreCoinConcurrent >= 4 || this.processPreCoinThrottle[preCoin.exchangeBaseUrl]) { + if ( + this.processPreCoinConcurrent >= 4 || + this.processPreCoinThrottle[preCoin.exchangeBaseUrl] + ) { console.log("delaying processPreCoin"); - this.timerGroup.after(retryDelayMs, - () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000))); + this.timerGroup.after(retryDelayMs, () => + this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)), + ); return; } console.log("executing processPreCoin", preCoin); this.processPreCoinConcurrent++; try { - const exchange = await this.q().get(Stores.exchanges, - preCoin.exchangeBaseUrl); + const exchange = await this.q().get( + Stores.exchanges, + preCoin.exchangeBaseUrl, + ); if (!exchange) { console.error("db inconsistent: exchange for precoin not found"); return; } - const denom = await this.q().get(Stores.denominations, - [preCoin.exchangeBaseUrl, preCoin.denomPub]); + const denom = await this.q().get(Stores.denominations, [ + preCoin.exchangeBaseUrl, + preCoin.denomPub, + ]); if (!denom) { console.error("db inconsistent: denom for precoin not found"); return; @@ -1031,11 +1142,17 @@ export class Wallet { console.log("processPreCoin: got coin", coin); const mutateReserve = (r: ReserveRecord) => { + console.log( + `before committing coin: current ${amountToPretty( + r.current_amount!, + )}, precoin: ${amountToPretty(r.precoin_amount)})}`, + ); - console.log(`before committing coin: current ${amountToPretty(r.current_amount!)}, precoin: ${amountToPretty( - r.precoin_amount)})}`); - - const x = Amounts.sub(r.precoin_amount, preCoin.coinValue, denom.feeWithdraw); + const x = Amounts.sub( + r.precoin_amount, + preCoin.coinValue, + denom.feeWithdraw, + ); if (x.saturated) { console.error("database inconsistent"); throw AbortTransaction; @@ -1045,15 +1162,20 @@ export class Wallet { }; await this.q() - .mutate(Stores.reserves, preCoin.reservePub, mutateReserve) - .delete(Stores.precoins, coin.coinPub) - .add(Stores.coins, coin) - .finish(); + .mutate(Stores.reserves, preCoin.reservePub, mutateReserve) + .delete(Stores.precoins, coin.coinPub) + .add(Stores.coins, coin) + .finish(); if (coin.status === CoinStatus.TainedByTip) { - const tip = await this.q().getIndexed(Stores.tips.coinPubIndex, coin.coinPub); + const tip = await this.q().getIndexed( + Stores.tips.coinPubIndex, + coin.coinPub, + ); if (!tip) { - throw Error(`inconsistent DB: tip for coin pub ${coin.coinPub} not found.`); + throw Error( + `inconsistent DB: tip for coin pub ${coin.coinPub} not found.`, + ); } if (tip.accepted) { @@ -1075,44 +1197,55 @@ export class Wallet { this.notifier.notify(); } catch (e) { - console.error("Failed to withdraw coin from precoin, retrying in", - retryDelayMs, - "ms", e); + console.error( + "Failed to withdraw coin from precoin, retrying in", + retryDelayMs, + "ms", + e, + ); // exponential backoff truncated at one minute const nextRetryDelayMs = Math.min(retryDelayMs * 2, 5 * 60 * 1000); - this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, nextRetryDelayMs)); - - const currentThrottle = this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0; - this.processPreCoinThrottle[preCoin.exchangeBaseUrl] = currentThrottle + 1; - this.timerGroup.after(retryDelayMs, () => {this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; }); + this.timerGroup.after(retryDelayMs, () => + this.processPreCoin(preCoin, nextRetryDelayMs), + ); + + const currentThrottle = + this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0; + this.processPreCoinThrottle[preCoin.exchangeBaseUrl] = + currentThrottle + 1; + this.timerGroup.after(retryDelayMs, () => { + this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; + }); } finally { this.processPreCoinConcurrent--; } } - /** * Update the timestamp of when an exchange was used. */ async updateExchangeUsedTime(exchangeBaseUrl: string): Promise<void> { - const now = (new Date()).getTime(); + const now = new Date().getTime(); const update = (r: ExchangeRecord) => { r.lastUsedTime = now; return r; }; - await this.q().mutate(Stores.exchanges, exchangeBaseUrl, update).finish(); + await this.q() + .mutate(Stores.exchanges, exchangeBaseUrl, update) + .finish(); } - /** * Create a reserve, but do not flag it as confirmed yet. * * Adds the corresponding exchange as a trusted exchange if it is neither * audited nor trusted already. */ - async createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> { + async createReserve( + req: CreateReserveRequest, + ): Promise<CreateReserveResponse> { const keypair = await this.cryptoApi.createEddsaKeypair(); - const now = (new Date()).getTime(); + const now = new Date().getTime(); const canonExchange = canonicalizeBaseUrl(req.exchange); const reserveRecord: ReserveRecord = { @@ -1134,13 +1267,18 @@ export class Wallet { const rec = { paytoUri: senderWire, }; - await this.q().put(Stores.senderWires, rec).finish(); + await this.q() + .put(Stores.senderWires, rec) + .finish(); } await this.updateExchangeUsedTime(req.exchange); const exchangeInfo = await this.updateExchangeFromUrl(req.exchange); - const {isAudited, isTrusted} = await this.getExchangeTrust(exchangeInfo); - let currencyRecord = await this.q().get(Stores.currencies, exchangeInfo.currency); + const { isAudited, isTrusted } = await this.getExchangeTrust(exchangeInfo); + let currencyRecord = await this.q().get( + Stores.currencies, + exchangeInfo.currency, + ); if (!currencyRecord) { currencyRecord = { auditors: [], @@ -1151,13 +1289,16 @@ export class Wallet { } if (!isAudited && !isTrusted) { - currencyRecord.exchanges.push({baseUrl: req.exchange, exchangePub: exchangeInfo.masterPublicKey}); + currencyRecord.exchanges.push({ + baseUrl: req.exchange, + exchangePub: exchangeInfo.masterPublicKey, + }); } await this.q() - .put(Stores.currencies, currencyRecord) - .put(Stores.reserves, reserveRecord) - .finish(); + .put(Stores.currencies, currencyRecord) + .put(Stores.reserves, reserveRecord) + .finish(); const r: CreateReserveResponse = { exchange: canonExchange, @@ -1166,7 +1307,6 @@ export class Wallet { return r; } - /** * Mark an existing reserve as confirmed. The wallet will start trying * to withdraw from that reserve. This may not immediately succeed, @@ -1177,10 +1317,10 @@ export class Wallet { * an unconfirmed reserve should be hidden. */ async confirmReserve(req: ConfirmReserveRequest): Promise<void> { - const now = (new Date()).getTime(); - const reserve: ReserveRecord|undefined = await ( - this.q().get<ReserveRecord>(Stores.reserves, - req.reservePub)); + const now = new Date().getTime(); + const reserve: ReserveRecord | undefined = await this.q().get< + ReserveRecord + >(Stores.reserves, req.reservePub); if (!reserve) { console.error("Unable to confirm reserve, not found in DB"); return; @@ -1188,21 +1328,20 @@ export class Wallet { console.log("reserve confirmed"); reserve.timestamp_confirmed = now; await this.q() - .put(Stores.reserves, reserve) - .finish(); + .put(Stores.reserves, reserve) + .finish(); this.notifier.notify(); this.processReserve(reserve); } - private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> { const wd: any = {}; wd.denom_pub_hash = pc.denomPubHash; wd.reserve_pub = pc.reservePub; wd.reserve_sig = pc.withdrawSig; wd.coin_ev = pc.coinEv; - const reqUrl = (new URI("reserve/withdraw")).absoluteTo(pc.exchangeBaseUrl); + const reqUrl = new URI("reserve/withdraw").absoluteTo(pc.exchangeBaseUrl); const resp = await this.http.postJson(reqUrl.href(), wd); if (resp.status !== 200) { @@ -1212,9 +1351,11 @@ export class Wallet { }); } const r = JSON.parse(resp.responseText); - const denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, - pc.blindingKey, - pc.denomPub); + const denomSig = await this.cryptoApi.rsaUnblind( + r.ev_sig, + pc.blindingKey, + pc.denomPub, + ); const coin: CoinRecord = { blindingKey: pc.blindingKey, coinPriv: pc.coinPriv, @@ -1230,7 +1371,6 @@ export class Wallet { return coin; } - /** * Withdraw coins from a reserve until it is empty. * @@ -1246,23 +1386,32 @@ export class Wallet { if (!withdrawAmount) { throw Error("can't withdraw when amount is unknown"); } - const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(reserve.exchange_base_url, withdrawAmount); - const smallestAmount = await this.getVerifiedSmallestWithdrawAmount(reserve.exchange_base_url); + const denomsForWithdraw = await this.getVerifiedWithdrawDenomList( + reserve.exchange_base_url, + withdrawAmount, + ); + const smallestAmount = await this.getVerifiedSmallestWithdrawAmount( + reserve.exchange_base_url, + ); console.log(`withdrawing ${denomsForWithdraw.length} coins`); - const ps = denomsForWithdraw.map(async(denom) => { + const ps = denomsForWithdraw.map(async denom => { function mutateReserve(r: ReserveRecord): ReserveRecord { const currentAmount = r.current_amount; if (!currentAmount) { throw Error("can't withdraw when amount is unknown"); } - r.precoin_amount = Amounts.add(r.precoin_amount, - denom.value, - denom.feeWithdraw).amount; - const result = Amounts.sub(currentAmount, - denom.value, - denom.feeWithdraw); + r.precoin_amount = Amounts.add( + r.precoin_amount, + denom.value, + denom.feeWithdraw, + ).amount; + const result = Amounts.sub( + currentAmount, + denom.value, + denom.feeWithdraw, + ); if (result.saturated) { console.error("can't create precoin, saturated"); throw AbortTransaction; @@ -1271,39 +1420,44 @@ export class Wallet { // Reserve is depleted if the amount left is too small to withdraw if (Amounts.cmp(r.current_amount, smallestAmount) < 0) { - r.timestamp_depleted = (new Date()).getTime(); + r.timestamp_depleted = new Date().getTime(); } - console.log(`after creating precoin: current ${amountToPretty(r.current_amount)}, precoin: ${amountToPretty( - r.precoin_amount)})}`); + console.log( + `after creating precoin: current ${amountToPretty( + r.current_amount, + )}, precoin: ${amountToPretty(r.precoin_amount)})}`, + ); return r; } - const preCoin = await this.cryptoApi - .createPreCoin(denom, reserve); + const preCoin = await this.cryptoApi.createPreCoin(denom, reserve); await this.q() - .put(Stores.precoins, preCoin) - .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve); + .put(Stores.precoins, preCoin) + .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve); await this.processPreCoin(preCoin); }); await Promise.all(ps); } - /** * Update the information about a reserve that is stored in the wallet * by quering the reserve's exchange. */ private async updateReserve(reservePub: string): Promise<ReserveRecord> { - const reserve = await this.q() - .get<ReserveRecord>(Stores.reserves, reservePub); + const reserve = await this.q().get<ReserveRecord>( + Stores.reserves, + reservePub, + ); if (!reserve) { throw Error("reserve not in db"); } - const reqUrl = new URI("reserve/status").absoluteTo(reserve.exchange_base_url); - reqUrl.query({reserve_pub: reservePub}); + const reqUrl = new URI("reserve/status").absoluteTo( + reserve.exchange_base_url, + ); + reqUrl.query({ reserve_pub: reservePub }); const resp = await this.http.get(reqUrl.href()); if (resp.status !== 200) { throw Error(); @@ -1314,13 +1468,12 @@ export class Wallet { } reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance); await this.q() - .put(Stores.reserves, reserve) - .finish(); + .put(Stores.reserves, reserve) + .finish(); this.notifier.notify(); return reserve; } - /** * Get the wire information for the exchange with the given base URL. */ @@ -1341,35 +1494,39 @@ export class Wallet { return ExchangeWireJson.checked(wiJson); } - async getPossibleDenoms(exchangeBaseUrl: string) { - return ( - this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, - exchangeBaseUrl) - .filter((d) => d.status === DenominationStatus.Unverified || d.status === DenominationStatus.VerifiedGood) - .toArray() - ); + return this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) + .filter( + d => + d.status === DenominationStatus.Unverified || + d.status === DenominationStatus.VerifiedGood, + ) + .toArray(); } - /** * Compute the smallest withdrawable amount possible, based on verified denominations. * * Writes to the DB in order to record the result from verifying * denominations. */ - async getVerifiedSmallestWithdrawAmount(exchangeBaseUrl: string): Promise<AmountJson> { + async getVerifiedSmallestWithdrawAmount( + exchangeBaseUrl: string, + ): Promise<AmountJson> { const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { throw Error(`exchange ${exchangeBaseUrl} not found`); } - const possibleDenoms = await ( - this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, - exchange.baseUrl) - .filter((d) => d.status === DenominationStatus.Unverified || d.status === DenominationStatus.VerifiedGood) - .toArray() - ); + const possibleDenoms = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .filter( + d => + d.status === DenominationStatus.Unverified || + d.status === DenominationStatus.VerifiedGood, + ) + .toArray(); possibleDenoms.sort((d1, d2) => { const a1 = Amounts.add(d1.feeWithdraw, d1.value).amount; const a2 = Amounts.add(d2.feeWithdraw, d2.value).amount; @@ -1381,14 +1538,18 @@ export class Wallet { return Amounts.add(denom.feeWithdraw, denom.value).amount; } console.log(`verifying denom ${denom.denomPub.substr(0, 15)}`); - const valid = await this.cryptoApi.isValidDenom(denom, - exchange.masterPublicKey); + const valid = await this.cryptoApi.isValidDenom( + denom, + exchange.masterPublicKey, + ); if (!valid) { denom.status = DenominationStatus.VerifiedBad; } else { denom.status = DenominationStatus.VerifiedGood; } - await this.q().put(Stores.denominations, denom).finish(); + await this.q() + .put(Stores.denominations, denom) + .finish(); if (valid) { return Amounts.add(denom.feeWithdraw, denom.value).amount; } @@ -1396,7 +1557,6 @@ export class Wallet { return Amounts.getZero(exchange.currency); } - /** * Get a list of denominations to withdraw from the given exchange for the * given amount, making sure that all denominations' signatures are verified. @@ -1404,19 +1564,23 @@ export class Wallet { * Writes to the DB in order to record the result from verifying * denominations. */ - async getVerifiedWithdrawDenomList(exchangeBaseUrl: string, - amount: AmountJson): Promise<DenominationRecord[]> { + async getVerifiedWithdrawDenomList( + exchangeBaseUrl: string, + amount: AmountJson, + ): Promise<DenominationRecord[]> { const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { throw Error(`exchange ${exchangeBaseUrl} not found`); } - const possibleDenoms = await ( - this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, - exchange.baseUrl) - .filter((d) => d.status === DenominationStatus.Unverified || d.status === DenominationStatus.VerifiedGood) - .toArray() - ); + const possibleDenoms = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .filter( + d => + d.status === DenominationStatus.Unverified || + d.status === DenominationStatus.VerifiedGood, + ) + .toArray(); let allValid = false; @@ -1429,8 +1593,10 @@ export class Wallet { for (const denom of selectedDenoms || []) { if (denom.status === DenominationStatus.Unverified) { console.log(`verifying denom ${denom.denomPub.substr(0, 15)}`); - const valid = await this.cryptoApi.isValidDenom(denom, - exchange.masterPublicKey); + const valid = await this.cryptoApi.isValidDenom( + denom, + exchange.masterPublicKey, + ); if (!valid) { denom.status = DenominationStatus.VerifiedBad; allValid = false; @@ -1438,7 +1604,9 @@ export class Wallet { denom.status = DenominationStatus.VerifiedGood; nextPossibleDenoms.push(denom); } - await this.q().put(Stores.denominations, denom).finish(); + await this.q() + .put(Stores.denominations, denom) + .finish(); } else { nextPossibleDenoms.push(denom); } @@ -1448,14 +1616,18 @@ export class Wallet { return selectedDenoms; } - /** * Check if and how an exchange is trusted and/or audited. */ - async getExchangeTrust(exchangeInfo: ExchangeRecord): Promise<{isTrusted: boolean, isAudited: boolean}> { + async getExchangeTrust( + exchangeInfo: ExchangeRecord, + ): Promise<{ isTrusted: boolean; isAudited: boolean }> { let isTrusted = false; let isAudited = false; - const currencyRecord = await this.q().get(Stores.currencies, exchangeInfo.currency); + const currencyRecord = await this.q().get( + Stores.currencies, + exchangeInfo.currency, + ); if (currencyRecord) { for (const trustedExchange of currencyRecord.exchanges) { if (trustedExchange.exchangePub === exchangeInfo.masterPublicKey) { @@ -1472,22 +1644,27 @@ export class Wallet { } } } - return {isTrusted, isAudited}; + return { isTrusted, isAudited }; } - async getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise<ReserveCreationInfo> { + async getReserveCreationInfo( + baseUrl: string, + amount: AmountJson, + ): Promise<ReserveCreationInfo> { const exchangeInfo = await this.updateExchangeFromUrl(baseUrl); - const selectedDenoms = await this.getVerifiedWithdrawDenomList(baseUrl, - amount); + const selectedDenoms = await this.getVerifiedWithdrawDenomList( + baseUrl, + amount, + ); let acc = Amounts.getZero(amount.currency); for (const d of selectedDenoms) { acc = Amounts.add(acc, d.feeWithdraw).amount; } const actualCoinCost = selectedDenoms - .map((d: DenominationRecord) => Amounts.add(d.value, - d.feeWithdraw).amount) + .map( + (d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount, + ) .reduce((a, b) => Amounts.add(a, b).amount); const wireInfo = await this.getWireInfo(baseUrl); @@ -1503,7 +1680,7 @@ export class Wallet { exchangeWireAccounts.push(account.url); } - const {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo); + const { isTrusted, isAudited } = await this.getExchangeTrust(exchangeInfo); let earliestDepositExpiration = Infinity; for (const denom of selectedDenoms) { @@ -1513,23 +1690,35 @@ export class Wallet { } } - const possibleDenoms = await ( - this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl) - .filter((d) => d.isOffered) - .toArray() - ) || []; + const possibleDenoms = + (await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl) + .filter(d => d.isOffered) + .toArray()) || []; const trustedAuditorPubs = []; - const currencyRecord = await this.q().get<CurrencyRecord>(Stores.currencies, amount.currency); + const currencyRecord = await this.q().get<CurrencyRecord>( + Stores.currencies, + amount.currency, + ); if (currencyRecord) { - trustedAuditorPubs.push(...currencyRecord.auditors.map((a) => a.auditorPub)); + trustedAuditorPubs.push( + ...currencyRecord.auditors.map(a => a.auditorPub), + ); } let versionMatch; if (exchangeInfo.protocolVersion) { - versionMatch = LibtoolVersion.compare(WALLET_PROTOCOL_VERSION, exchangeInfo.protocolVersion); - - if (versionMatch && !versionMatch.compatible && versionMatch.currentCmp === -1) { + versionMatch = LibtoolVersion.compare( + WALLET_PROTOCOL_VERSION, + exchangeInfo.protocolVersion, + ); + + if ( + versionMatch && + !versionMatch.compatible && + versionMatch.currentCmp === -1 + ) { console.log("wallet version might be outdated, checking for updates"); chrome.runtime.requestUpdateCheck((status, details) => { console.log("update check status:", status); @@ -1556,7 +1745,6 @@ export class Wallet { return ret; } - /** * Update or add exchange DB entry by fetching the /keys information. * Optionally link the reserve entry to the new or existing @@ -1569,25 +1757,32 @@ export class Wallet { if (keysResp.status !== 200) { throw Error("/keys request failed"); } - const exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText)); + const exchangeKeysJson = KeysJson.checked( + JSON.parse(keysResp.responseText), + ); const exchangeWire = await this.getWireInfo(baseUrl); return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire); } - private async suspendCoins(exchangeInfo: ExchangeRecord): Promise<void> { - const resultSuspendedCoins = await ( - this.q() - .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl) - .indexJoinLeft(Stores.denominations.exchangeBaseUrlIndex, - (e) => e.exchangeBaseUrl) - .fold((cd: JoinLeftResult<CoinRecord, DenominationRecord>, - suspendedCoins: CoinRecord[]) => { - if ((!cd.right) || (!cd.right.isOffered)) { - return Array.prototype.concat(suspendedCoins, [cd.left]); - } - return Array.prototype.concat(suspendedCoins); - }, [])); + const resultSuspendedCoins = await this.q() + .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl) + .indexJoinLeft( + Stores.denominations.exchangeBaseUrlIndex, + e => e.exchangeBaseUrl, + ) + .fold( + ( + cd: JoinLeftResult<CoinRecord, DenominationRecord>, + suspendedCoins: CoinRecord[], + ) => { + if (!cd.right || !cd.right.isOffered) { + return Array.prototype.concat(suspendedCoins, [cd.left]); + } + return Array.prototype.concat(suspendedCoins); + }, + [], + ); const q = this.q(); resultSuspendedCoins.map((c: CoinRecord) => { @@ -1600,11 +1795,11 @@ export class Wallet { await q.finish(); } - - private async updateExchangeFromJson(baseUrl: string, - exchangeKeysJson: KeysJson, - wireMethodDetails: ExchangeWireJson): Promise<ExchangeRecord> { - + private async updateExchangeFromJson( + baseUrl: string, + exchangeKeysJson: KeysJson, + wireMethodDetails: ExchangeWireJson, + ): Promise<ExchangeRecord> { // FIXME: all this should probably be commited atomically const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); if (updateTimeSec === null) { @@ -1625,7 +1820,8 @@ export class Wallet { exchangeInfo = { auditors: exchangeKeysJson.auditors, baseUrl, - currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value).currency, + currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value) + .currency, lastUpdateTime: updateTimeSec, lastUsedTime: 0, masterPublicKey: exchangeKeysJson.master_public_key, @@ -1641,14 +1837,16 @@ export class Wallet { console.log("updating old exchange"); } - const updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo, - exchangeKeysJson); + const updatedExchangeInfo = await this.updateExchangeInfo( + exchangeInfo, + exchangeKeysJson, + ); await this.suspendCoins(updatedExchangeInfo); updatedExchangeInfo.protocolVersion = exchangeKeysJson.version; await this.q() - .put(Stores.exchanges, updatedExchangeInfo) - .finish(); + .put(Stores.exchanges, updatedExchangeInfo) + .finish(); let oldWireFees = await this.q().get(Stores.exchangeWireFees, baseUrl); if (!oldWireFees) { @@ -1689,7 +1887,11 @@ export class Wallet { startStamp: start, wireFee: Amounts.parseOrThrow(fee.wire_fee), }; - const valid: boolean = await this.cryptoApi.isValidWireFee(paytoTargetType, wf, exchangeInfo.masterPublicKey); + const valid: boolean = await this.cryptoApi.isValidWireFee( + paytoTargetType, + wf, + exchangeInfo.masterPublicKey, + ); if (!valid) { console.error("fee signature invalid", fee); throw Error("fee signature invalid"); @@ -1702,12 +1904,17 @@ export class Wallet { if (exchangeKeysJson.payback) { for (const payback of exchangeKeysJson.payback) { - const denom = await this.q().getIndexed(Stores.denominations.denomPubHashIndex, payback.h_denom_pub); + const denom = await this.q().getIndexed( + Stores.denominations.denomPubHashIndex, + payback.h_denom_pub, + ); if (!denom) { continue; } console.log(`cashing back denom`, denom); - const coins = await this.q().iterIndex(Stores.coins.denomPubIndex, denom.denomPub).toArray(); + const coins = await this.q() + .iterIndex(Stores.coins.denomPubIndex, denom.denomPub) + .toArray(); for (const coin of coins) { this.payback(coin.coinPub); } @@ -1717,19 +1924,27 @@ export class Wallet { return updatedExchangeInfo; } - - private async updateExchangeInfo(exchangeInfo: ExchangeRecord, - newKeys: KeysJson): Promise<ExchangeRecord> { + private async updateExchangeInfo( + exchangeInfo: ExchangeRecord, + newKeys: KeysJson, + ): Promise<ExchangeRecord> { if (exchangeInfo.masterPublicKey !== newKeys.master_public_key) { throw Error("public keys do not match"); } - const existingDenoms: {[denomPub: string]: DenominationRecord} = await ( - this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, - exchangeInfo.baseUrl) - .fold((x: DenominationRecord, - acc: typeof existingDenoms) => (acc[x.denomPub] = x, acc), {}) - ); + const existingDenoms: { + [denomPub: string]: DenominationRecord; + } = await this.q() + .iterIndex( + Stores.denominations.exchangeBaseUrlIndex, + exchangeInfo.baseUrl, + ) + .fold( + (x: DenominationRecord, acc: typeof existingDenoms) => ( + (acc[x.denomPub] = x), acc + ), + {}, + ); const newDenoms: typeof existingDenoms = {}; const newAndUnseenDenoms: typeof existingDenoms = {}; @@ -1750,15 +1965,18 @@ export class Wallet { } await this.q() - .putAll(Stores.denominations, - Object.keys(newAndUnseenDenoms).map((d) => newAndUnseenDenoms[d])) - .putAll(Stores.denominations, - Object.keys(existingDenoms).map((d) => existingDenoms[d])) - .finish(); + .putAll( + Stores.denominations, + Object.keys(newAndUnseenDenoms).map(d => newAndUnseenDenoms[d]), + ) + .putAll( + Stores.denominations, + Object.keys(existingDenoms).map(d => existingDenoms[d]), + ) + .finish(); return exchangeInfo; } - /** * Get detailed balance information, sliced by exchange and by currency. */ @@ -1767,15 +1985,24 @@ export class Wallet { * Add amount to a balance field, both for * the slicing by exchange and currency. */ - function addTo(balance: WalletBalance, - field: keyof WalletBalanceEntry, - amount: AmountJson, - exchange: string): void { + function addTo( + balance: WalletBalance, + field: keyof WalletBalanceEntry, + amount: AmountJson, + exchange: string, + ): void { const z = Amounts.getZero(amount.currency); - const balanceIdentity = {available: z, paybackAmount: z, pendingIncoming: z, pendingPayment: z}; + const balanceIdentity = { + available: z, + paybackAmount: z, + pendingIncoming: z, + pendingPayment: z, + }; let entryCurr = balance.byCurrency[amount.currency]; if (!entryCurr) { - balance.byCurrency[amount.currency] = entryCurr = { ...balanceIdentity }; + balance.byCurrency[amount.currency] = entryCurr = { + ...balanceIdentity, + }; } let entryEx = balance.byExchange[exchange]; if (!entryEx) { @@ -1819,14 +2046,19 @@ export class Wallet { if (!r.hasPayback) { return balance; } - if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) < 0) { + if ( + Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) < + 0 + ) { addTo(balance, "paybackAmount", r.current_amount!, r.exchange_base_url); } return balance; } - function collectPendingRefresh(r: RefreshSessionRecord, - balance: WalletBalance) { + function collectPendingRefresh( + r: RefreshSessionRecord, + balance: WalletBalance, + ) { // Don't count finished refreshes, since the refresh already resulted // in coins being added to the wallet. if (r.finished) { @@ -1842,13 +2074,20 @@ export class Wallet { return balance; } for (const c of t.payReq.coins) { - addTo(balance, "pendingPayment", Amounts.parseOrThrow(c.contribution), c.exchange_url); + addTo( + balance, + "pendingPayment", + Amounts.parseOrThrow(c.contribution), + c.exchange_url, + ); } return balance; } - function collectSmallestWithdraw(e: JoinResult<ExchangeRecord, DenominationRecord>, - sw: any) { + function collectSmallestWithdraw( + e: JoinResult<ExchangeRecord, DenominationRecord>, + sw: any, + ) { let min = sw[e.left.baseUrl]; const v = Amounts.add(e.right.value, e.right.feeWithdraw).amount; if (!min) { @@ -1866,31 +2105,26 @@ export class Wallet { }; // Mapping from exchange pub to smallest // possible amount we can withdraw - let smallestWithdraw: {[baseUrl: string]: AmountJson} = {}; + let smallestWithdraw: { [baseUrl: string]: AmountJson } = {}; - smallestWithdraw = await (this.q() - .iter(Stores.exchanges) - .indexJoin(Stores.denominations.exchangeBaseUrlIndex, - (x) => x.baseUrl) - .fold(collectSmallestWithdraw, {})); + smallestWithdraw = await this.q() + .iter(Stores.exchanges) + .indexJoin(Stores.denominations.exchangeBaseUrlIndex, x => x.baseUrl) + .fold(collectSmallestWithdraw, {}); const tx = this.q(); - tx.iter(Stores.coins) - .fold(collectBalances, balanceStore); - tx.iter(Stores.refresh) - .fold(collectPendingRefresh, balanceStore); - tx.iter(Stores.reserves) - .fold(collectPendingWithdraw, balanceStore); - tx.iter(Stores.reserves) - .fold(collectPaybacks, balanceStore); - tx.iter(Stores.purchases) - .fold(collectPayments, balanceStore); + tx.iter(Stores.coins).fold(collectBalances, balanceStore); + tx.iter(Stores.refresh).fold(collectPendingRefresh, balanceStore); + tx.iter(Stores.reserves).fold(collectPendingWithdraw, balanceStore); + tx.iter(Stores.reserves).fold(collectPaybacks, balanceStore); + tx.iter(Stores.purchases).fold(collectPayments, balanceStore); await tx.finish(); return balanceStore; } - - async createRefreshSession(oldCoinPub: string): Promise<RefreshSessionRecord|undefined> { + async createRefreshSession( + oldCoinPub: string, + ): Promise<RefreshSessionRecord | undefined> { const coin = await this.q().get<CoinRecord>(Stores.coins, oldCoinPub); if (!coin) { @@ -1907,48 +2141,52 @@ export class Wallet { throw Error("db inconsistent"); } - const oldDenom = await this.q().get(Stores.denominations, - [exchange.baseUrl, coin.denomPub]); + const oldDenom = await this.q().get(Stores.denominations, [ + exchange.baseUrl, + coin.denomPub, + ]); if (!oldDenom) { throw Error("db inconsistent"); } - const availableDenoms: DenominationRecord[] = await ( - this.q() - .iterIndex(Stores.denominations.exchangeBaseUrlIndex, - exchange.baseUrl) - .toArray() - ); + const availableDenoms: DenominationRecord[] = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) + .toArray(); - const availableAmount = Amounts.sub(coin.currentAmount, - oldDenom.feeRefresh).amount; + const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh) + .amount; - const newCoinDenoms = getWithdrawDenomList(availableAmount, - availableDenoms); + const newCoinDenoms = getWithdrawDenomList( + availableAmount, + availableDenoms, + ); console.log("refreshing coin", coin); console.log("refreshing into", newCoinDenoms); if (newCoinDenoms.length === 0) { - console.log(`not refreshing, available amount ${amountToPretty(availableAmount)} too small`); + console.log( + `not refreshing, available amount ${amountToPretty( + availableAmount, + )} too small`, + ); coin.status = CoinStatus.Useless; await this.q().put(Stores.coins, coin); this.notifier.notify(); return undefined; } - - const refreshSession: RefreshSessionRecord = await ( - this.cryptoApi.createRefreshSession(exchange.baseUrl, - 3, - coin, - newCoinDenoms, - oldDenom.feeRefresh)); + const refreshSession: RefreshSessionRecord = await this.cryptoApi.createRefreshSession( + exchange.baseUrl, + 3, + coin, + newCoinDenoms, + oldDenom.feeRefresh, + ); function mutateCoin(c: CoinRecord): CoinRecord { - const r = Amounts.sub(c.currentAmount, - refreshSession.valueWithFee); + const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee); if (r.saturated) { // Something else must have written the coin value throw AbortTransaction; @@ -1961,8 +2199,9 @@ export class Wallet { // Store refresh session and subtract refreshed amount from // coin in the same transaction. const query = this.q(); - query.put(Stores.refresh, refreshSession, "refreshKey") - .mutate(Stores.coins, coin.coinPub, mutateCoin); + query + .put(Stores.refresh, refreshSession, "refreshKey") + .mutate(Stores.coins, coin.coinPub, mutateCoin); await query.finish(); this.notifier.notify(); @@ -1976,10 +2215,10 @@ export class Wallet { return refreshSession; } - async refresh(oldCoinPub: string): Promise<void> { - - const oldRefreshSessions = await this.q().iter(Stores.refresh).toArray(); + const oldRefreshSessions = await this.q() + .iter(Stores.refresh) + .toArray(); for (const session of oldRefreshSessions) { console.log("got old session for", oldCoinPub, session); this.continueRefreshSession(session); @@ -1989,7 +2228,10 @@ export class Wallet { console.warn("can't refresh, coin not in database"); return; } - if (coin.status === CoinStatus.Useless || coin.status === CoinStatus.Fresh) { + if ( + coin.status === CoinStatus.Useless || + coin.status === CoinStatus.Fresh + ) { return; } const refreshSession = await this.createRefreshSession(oldCoinPub); @@ -2007,7 +2249,10 @@ export class Wallet { } if (typeof refreshSession.norevealIndex !== "number") { await this.refreshMelt(refreshSession); - const r = await this.q().get<RefreshSessionRecord>(Stores.refresh, refreshSession.id); + const r = await this.q().get<RefreshSessionRecord>( + Stores.refresh, + refreshSession.id, + ); if (!r) { throw Error("refresh session does not exist anymore"); } @@ -2017,21 +2262,24 @@ export class Wallet { await this.refreshReveal(refreshSession); } - async refreshMelt(refreshSession: RefreshSessionRecord): Promise<void> { if (refreshSession.norevealIndex !== undefined) { console.error("won't melt again"); return; } - const coin = await this.q().get<CoinRecord>(Stores.coins, - refreshSession.meltCoinPub); + const coin = await this.q().get<CoinRecord>( + Stores.coins, + refreshSession.meltCoinPub, + ); if (!coin) { console.error("can't melt coin, it does not exist"); return; } - const reqUrl = new URI("refresh/melt").absoluteTo(refreshSession.exchangeBaseUrl); + const reqUrl = new URI("refresh/melt").absoluteTo( + refreshSession.exchangeBaseUrl, + ); const meltReq = { coin_pub: coin.coinPub, confirm_sig: refreshSession.confirmSig, @@ -2064,11 +2312,12 @@ export class Wallet { refreshSession.norevealIndex = norevealIndex; - await this.q().put(Stores.refresh, refreshSession).finish(); + await this.q() + .put(Stores.refresh, refreshSession) + .finish(); this.notifier.notify(); } - async refreshReveal(refreshSession: RefreshSessionRecord): Promise<void> { const norevealIndex = refreshSession.norevealIndex; if (norevealIndex === undefined) { @@ -2082,17 +2331,40 @@ export class Wallet { throw Error("refresh index error"); } + const meltCoinRecord = await this.q().get( + Stores.coins, + refreshSession.meltCoinPub, + ); + if (!meltCoinRecord) { + throw Error("inconsistent database"); + } + const evs = preCoins.map((x: RefreshPreCoinRecord) => x.coinEv); + const linkSigs: string[] = []; + for (let i = 0; i < refreshSession.newDenoms.length; i++) { + const linkSig = await this.cryptoApi.signCoinLink( + meltCoinRecord.coinPriv, + refreshSession.newDenomHashes[i], + refreshSession.meltCoinPub, + refreshSession.transferPubs[norevealIndex], + preCoins[i].coinEv, + ); + linkSigs.push(linkSig); + } + const req = { coin_evs: evs, new_denoms_h: refreshSession.newDenomHashes, rc: refreshSession.hash, transfer_privs: privs, transfer_pub: refreshSession.transferPubs[norevealIndex], + link_sigs: linkSigs, }; - const reqUrl = new URI("refresh/reveal") .absoluteTo(refreshSession.exchangeBaseUrl); + const reqUrl = new URI("refresh/reveal").absoluteTo( + refreshSession.exchangeBaseUrl, + ); console.log("reveal request:", req); const resp = await this.http.postJson(reqUrl.href(), req); @@ -2110,8 +2382,10 @@ export class Wallet { console.log("/refresh/reveal did not contain ev_sigs"); } - const exchange = await this.q().get<ExchangeRecord>(Stores.exchanges, - refreshSession.exchangeBaseUrl); + const exchange = await this.q().get<ExchangeRecord>( + Stores.exchanges, + refreshSession.exchangeBaseUrl, + ); if (!exchange) { console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`); return; @@ -2120,21 +2394,21 @@ export class Wallet { const coins: CoinRecord[] = []; for (let i = 0; i < respJson.ev_sigs.length; i++) { - const denom = await ( - this.q() - .get(Stores.denominations, - [ - refreshSession.exchangeBaseUrl, - refreshSession.newDenoms[i], - ])); + const denom = await this.q().get(Stores.denominations, [ + refreshSession.exchangeBaseUrl, + refreshSession.newDenoms[i], + ]); if (!denom) { console.error("denom not found"); continue; } - const pc = refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i]; - const denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig, - pc.blindingKey, - denom.denomPub); + const pc = + refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i]; + const denomSig = await this.cryptoApi.rsaUnblind( + respJson.ev_sigs[i].ev_sig, + pc.blindingKey, + denom.denomPub, + ); const coin: CoinRecord = { blindingKey: pc.blindingKey, coinPriv: pc.privateKey, @@ -2154,22 +2428,23 @@ export class Wallet { refreshSession.finished = true; await this.q() - .putAll(Stores.coins, coins) - .put(Stores.refresh, refreshSession) - .finish(); + .putAll(Stores.coins, coins) + .put(Stores.refresh, refreshSession) + .finish(); this.notifier.notify(); } - /** * Retrive the full event history for this wallet. */ - async getHistory(): Promise<{history: HistoryRecord[]}> { + async getHistory(): Promise<{ history: HistoryRecord[] }> { const history: HistoryRecord[] = []; // FIXME: do pagination instead of generating the full history - const proposals = await this.q().iter<ProposalDownloadRecord>(Stores.proposals).toArray(); + const proposals = await this.q() + .iter<ProposalDownloadRecord>(Stores.proposals) + .toArray(); for (const p of proposals) { history.push({ detail: { @@ -2181,7 +2456,9 @@ export class Wallet { }); } - const purchases = await this.q().iter<PurchaseRecord>(Stores.purchases).toArray(); + const purchases = await this.q() + .iter<PurchaseRecord>(Stores.purchases) + .toArray(); for (const p of purchases) { history.push({ detail: { @@ -2195,16 +2472,17 @@ export class Wallet { }); if (p.timestamp_refund) { const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount); - const amountsPending = ( - Object.keys(p.refundsPending) - .map((x) => Amounts.parseOrThrow(p.refundsPending[x].refund_amount)) + const amountsPending = Object.keys(p.refundsPending).map(x => + Amounts.parseOrThrow(p.refundsPending[x].refund_amount), ); - const amountsDone = ( - Object.keys(p.refundsDone) - .map((x) => Amounts.parseOrThrow(p.refundsDone[x].refund_amount)) + const amountsDone = Object.keys(p.refundsDone).map(x => + Amounts.parseOrThrow(p.refundsDone[x].refund_amount), ); const amounts: AmountJson[] = amountsPending.concat(amountsDone); - const amount = Amounts.add(Amounts.getZero(contractAmount.currency), ...amounts).amount; + const amount = Amounts.add( + Amounts.getZero(contractAmount.currency), + ...amounts, + ).amount; history.push({ detail: { @@ -2219,7 +2497,9 @@ export class Wallet { } } - const reserves: ReserveRecord[] = await this.q().iter<ReserveRecord>(Stores.reserves).toArray(); + const reserves: ReserveRecord[] = await this.q() + .iter<ReserveRecord>(Stores.reserves) + .toArray(); for (const r of reserves) { history.push({ detail: { @@ -2243,7 +2523,9 @@ export class Wallet { } } - const tips: TipRecord[] = await this.q().iter<TipRecord>(Stores.tips).toArray(); + const tips: TipRecord[] = await this.q() + .iter<TipRecord>(Stores.tips) + .toArray(); for (const tip of tips) { history.push({ detail: { @@ -2259,70 +2541,74 @@ export class Wallet { history.sort((h1, h2) => Math.sign(h1.timestamp - h2.timestamp)); - return {history}; + return { history }; } async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> { - const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl).toArray(); + const denoms = await this.q() + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl) + .toArray(); return denoms; } - async getProposal(proposalId: number): Promise<ProposalDownloadRecord|undefined> { + async getProposal( + proposalId: number, + ): Promise<ProposalDownloadRecord | undefined> { const proposal = await this.q().get(Stores.proposals, proposalId); return proposal; } async getExchanges(): Promise<ExchangeRecord[]> { return this.q() - .iter<ExchangeRecord>(Stores.exchanges) - .toArray(); + .iter<ExchangeRecord>(Stores.exchanges) + .toArray(); } async getCurrencies(): Promise<CurrencyRecord[]> { return this.q() - .iter<CurrencyRecord>(Stores.currencies) - .toArray(); + .iter<CurrencyRecord>(Stores.currencies) + .toArray(); } async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> { console.log("updating currency to", currencyRecord); await this.q() - .put(Stores.currencies, currencyRecord) - .finish(); + .put(Stores.currencies, currencyRecord) + .finish(); this.notifier.notify(); } async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { return this.q() - .iter<ReserveRecord>(Stores.reserves) - .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl) - .toArray(); + .iter<ReserveRecord>(Stores.reserves) + .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl) + .toArray(); } async getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> { return this.q() - .iter<CoinRecord>(Stores.coins) - .filter((c: CoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) - .toArray(); + .iter<CoinRecord>(Stores.coins) + .filter((c: CoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); } async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> { return this.q() - .iter<PreCoinRecord>(Stores.precoins) - .filter((c: PreCoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) - .toArray(); + .iter<PreCoinRecord>(Stores.precoins) + .filter((c: PreCoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); } async hashContract(contract: ContractTerms): Promise<string> { return this.cryptoApi.hashString(canonicalJson(contract)); } - - async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> { + async getCurrencyRecord( + currency: string, + ): Promise<CurrencyRecord | undefined> { return this.q().get(Stores.currencies, currency); } - async payback(coinPub: string): Promise<void> { let coin = await this.q().get(Stores.coins, coinPub); if (!coin) { @@ -2338,7 +2624,9 @@ export class Wallet { } switch (coin.status) { case CoinStatus.Refreshed: - throw Error(`Can't do payback for coin ${coinPub} since it's refreshed`); + throw Error( + `Can't do payback for coin ${coinPub} since it's refreshed`, + ); case CoinStatus.PaybackDone: console.log(`Coin ${coinPub} already payed back`); return; @@ -2348,7 +2636,9 @@ export class Wallet { // technically we might update reserve status before we get the response // from the reserve for the payback request. reserve.hasPayback = true; - await this.q().put(Stores.coins, coin).put(Stores.reserves, reserve); + await this.q() + .put(Stores.coins, coin) + .put(Stores.reserves, reserve); this.notifier.notify(); const paybackRequest = await this.cryptoApi.createPaybackRequest(coin); @@ -2357,7 +2647,9 @@ export class Wallet { if (resp.status !== 200) { throw Error(); } - const paybackConfirmation = PaybackConfirmation.checked(JSON.parse(resp.responseText)); + const paybackConfirmation = PaybackConfirmation.checked( + JSON.parse(resp.responseText), + ); if (paybackConfirmation.reserve_pub !== coin.reservePub) { throw Error(`Coin's reserve doesn't match reserve on payback`); } @@ -2371,8 +2663,10 @@ export class Wallet { await this.updateReserve(reservePub!); } - - async denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): Promise<DenominationRecord> { + async denominationRecordFromKeys( + exchangeBaseUrl: string, + denomIn: Denomination, + ): Promise<DenominationRecord> { const denomPubHash = await this.cryptoApi.hashDenomPub(denomIn.denom_pub); const d: DenominationRecord = { denomPub: denomIn.denom_pub, @@ -2405,7 +2699,10 @@ export class Wallet { } async getPaybackReserves(): Promise<ReserveRecord[]> { - return await this.q().iter(Stores.reserves).filter((r) => r.hasPayback).toArray(); + return await this.q() + .iter(Stores.reserves) + .filter(r => r.hasPayback) + .toArray(); } /** @@ -2417,18 +2714,26 @@ export class Wallet { async getSenderWireInfos(): Promise<SenderWireInfos> { const m: { [url: string]: Set<string> } = {}; - await this.q().iter(Stores.exchangeWireFees).map((x) => { - const s = m[x.exchangeBaseUrl] = m[x.exchangeBaseUrl] || new Set(); - Object.keys(x.feesForType).map((k) => s.add(k)); - }).run(); + await this.q() + .iter(Stores.exchangeWireFees) + .map(x => { + const s = (m[x.exchangeBaseUrl] = m[x.exchangeBaseUrl] || new Set()); + Object.keys(x.feesForType).map(k => s.add(k)); + }) + .run(); console.log(m); const exchangeWireTypes: { [url: string]: string[] } = {}; - Object.keys(m).map((e) => { exchangeWireTypes[e] = Array.from(m[e]); }); + Object.keys(m).map(e => { + exchangeWireTypes[e] = Array.from(m[e]); + }); const senderWiresSet = new Set(); - await this.q().iter(Stores.senderWires).map((x) => { + await this.q() + .iter(Stores.senderWires) + .map(x => { senderWiresSet.add(x.paytoUri); - }).run(); + }) + .run(); const senderWires = Array.from(senderWiresSet); return { @@ -2448,7 +2753,7 @@ export class Wallet { console.error(`wire type must be a non-empty string, not ${wireType}`); return; } - const stampSecNow = Math.floor((new Date()).getTime() / 1000); + const stampSecNow = Math.floor(new Date().getTime() / 1000); const exchange = await this.q().get(Stores.exchanges, req.exchange); if (!exchange) { console.error(`Exchange ${req.exchange} not known to the wallet`); @@ -2464,13 +2769,17 @@ export class Wallet { const { priv, pub } = await this.cryptoApi.createEddsaKeypair(); - const wireHash = await this.cryptoApi.hashString(canonicalJson(req.senderWire)); + const wireHash = await this.cryptoApi.hashString( + canonicalJson(req.senderWire), + ); const contractTerms: ContractTerms = { H_wire: wireHash, amount: Amounts.toString(req.amount), auditors: [], - exchanges: [ { master_pub: exchange.masterPublicKey, url: exchange.baseUrl } ], + exchanges: [ + { master_pub: exchange.masterPublicKey, url: exchange.baseUrl }, + ], extra: {}, fulfillment_url: "", locations: [], @@ -2486,15 +2795,19 @@ export class Wallet { wire_method: wireType, }; - const contractTermsHash = await this.cryptoApi.hashString(canonicalJson(contractTerms)); + const contractTermsHash = await this.cryptoApi.hashString( + canonicalJson(contractTerms), + ); - const payCoinInfo = await ( - this.cryptoApi.signDeposit(contractTerms, cds, Amounts.parseOrThrow(contractTerms.amount)) + const payCoinInfo = await this.cryptoApi.signDeposit( + contractTerms, + cds, + Amounts.parseOrThrow(contractTerms.amount), ); console.log("pci", payCoinInfo); - const coins = payCoinInfo.sigs.map((s) => ({ coinPaySig: s })); + const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s })); const coinsReturnRecord: CoinsReturnRecord = { coins, @@ -2506,16 +2819,18 @@ export class Wallet { }; await this.q() - .put(Stores.coinsReturns, coinsReturnRecord) - .putAll(Stores.coins, payCoinInfo.updatedCoins) - .finish(); + .put(Stores.coinsReturns, coinsReturnRecord) + .putAll(Stores.coins, payCoinInfo.updatedCoins) + .finish(); this.badge.showNotification(); this.notifier.notify(); this.depositReturnedCoins(coinsReturnRecord); } - async depositReturnedCoins(coinsReturnRecord: CoinsReturnRecord): Promise<void> { + async depositReturnedCoins( + coinsReturnRecord: CoinsReturnRecord, + ): Promise<void> { for (const c of coinsReturnRecord.coins) { if (c.depositedSig) { continue; @@ -2536,7 +2851,7 @@ export class Wallet { wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline, }; console.log("req", req); - const reqUrl = (new URI("deposit")).absoluteTo(coinsReturnRecord.exchange); + const reqUrl = new URI("deposit").absoluteTo(coinsReturnRecord.exchange); const resp = await this.http.postJson(reqUrl.href(), req); if (resp.status !== 200) { console.error("deposit failed due to status code", resp); @@ -2556,7 +2871,10 @@ export class Wallet { // FIXME: verify signature // For every successful deposit, we replace the old record with an updated one - const currentCrr = await this.q().get(Stores.coinsReturns, coinsReturnRecord.contractTermsHash); + const currentCrr = await this.q().get( + Stores.coinsReturns, + coinsReturnRecord.contractTermsHash, + ); if (!currentCrr) { console.error("database inconsistent"); continue; @@ -2571,7 +2889,9 @@ export class Wallet { } } - async acceptRefundResponse(refundResponse: MerchantRefundResponse): Promise<string> { + async acceptRefundResponse( + refundResponse: MerchantRefundResponse, + ): Promise<string> { const refundPermissions = refundResponse.refund_permissions; if (!refundPermissions.length) { @@ -2582,16 +2902,19 @@ export class Wallet { /** * Add refund to purchase if not already added. */ - function f(t: PurchaseRecord|undefined): PurchaseRecord|undefined { + function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined { if (!t) { console.error("purchase not found, not adding refunds"); return; } - t.timestamp_refund = (new Date()).getTime(); + t.timestamp_refund = new Date().getTime(); for (const perm of refundPermissions) { - if (!t.refundsPending[perm.merchant_sig] && !t.refundsDone[perm.merchant_sig]) { + if ( + !t.refundsPending[perm.merchant_sig] && + !t.refundsDone[perm.merchant_sig] + ) { t.refundsPending[perm.merchant_sig] = perm; } } @@ -2601,7 +2924,9 @@ export class Wallet { const hc = refundResponse.h_contract_terms; // Add the refund permissions to the purchase within a DB transaction - await this.q().mutate(Stores.purchases, hc, f).finish(); + await this.q() + .mutate(Stores.purchases, hc, f) + .finish(); this.notifier.notify(); // Start submitting it but don't wait for it here. @@ -2610,7 +2935,6 @@ export class Wallet { return hc; } - /** * Accept a refund, return the contract hash for the contract * that was involved in the refund. @@ -2632,11 +2956,13 @@ export class Wallet { return this.acceptRefundResponse(refundResponse); } - private async submitRefunds(contractTermsHash: string): Promise<void> { const purchase = await this.q().get(Stores.purchases, contractTermsHash); if (!purchase) { - console.error("not submitting refunds, contract terms not found:", contractTermsHash); + console.error( + "not submitting refunds, contract terms not found:", + contractTermsHash, + ); return; } const pendingKeys = Object.keys(purchase.refundsPending); @@ -2657,7 +2983,7 @@ export class Wallet { console.log("sending refund permission", perm); // FIXME: not correct once we support multiple exchanges per payment const exchangeUrl = purchase.payReq.coins[0].exchange_url; - const reqUrl = (new URI("refund")).absoluteTo(exchangeUrl); + const reqUrl = new URI("refund").absoluteTo(exchangeUrl); const resp = await this.http.postJson(reqUrl.href(), req); if (resp.status !== 200) { console.error("refund failed", resp); @@ -2665,7 +2991,9 @@ export class Wallet { } // Transactionally mark successful refunds as done - const transformPurchase = (t: PurchaseRecord|undefined): PurchaseRecord|undefined => { + const transformPurchase = ( + t: PurchaseRecord | undefined, + ): PurchaseRecord | undefined => { if (!t) { console.warn("purchase not found, not updating refund"); return; @@ -2676,7 +3004,9 @@ export class Wallet { } return t; }; - const transformCoin = (c: CoinRecord|undefined): CoinRecord|undefined => { + const transformCoin = ( + c: CoinRecord | undefined, + ): CoinRecord | undefined => { if (!c) { console.warn("coin not found, can't apply refund"); return; @@ -2691,9 +3021,9 @@ export class Wallet { }; await this.q() - .mutate(Stores.purchases, contractTermsHash, transformPurchase) - .mutate(Stores.coins, perm.coin_pub, transformCoin) - .finish(); + .mutate(Stores.purchases, contractTermsHash, transformPurchase) + .mutate(Stores.coins, perm.coin_pub, transformCoin) + .finish(); this.refresh(perm.coin_pub); } @@ -2701,27 +3031,44 @@ export class Wallet { this.notifier.notify(); } - async getPurchase(contractTermsHash: string): Promise<PurchaseRecord|undefined> { + async getPurchase( + contractTermsHash: string, + ): Promise<PurchaseRecord | undefined> { return this.q().get(Stores.purchases, contractTermsHash); } - async getFullRefundFees(refundPermissions: MerchantRefundPermission[]): Promise<AmountJson> { + async getFullRefundFees( + refundPermissions: MerchantRefundPermission[], + ): Promise<AmountJson> { if (refundPermissions.length === 0) { throw Error("no refunds given"); } - const coin0 = await this.q().get(Stores.coins, refundPermissions[0].coin_pub); + const coin0 = await this.q().get( + Stores.coins, + refundPermissions[0].coin_pub, + ); if (!coin0) { throw Error("coin not found"); } - let feeAcc = Amounts.getZero(Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency); + let feeAcc = Amounts.getZero( + Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency, + ); - const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl).toArray(); + const denoms = await this.q() + .iterIndex( + Stores.denominations.exchangeBaseUrlIndex, + coin0.exchangeBaseUrl, + ) + .toArray(); for (const rp of refundPermissions) { const coin = await this.q().get(Stores.coins, rp.coin_pub); if (!coin) { throw Error("coin not found"); } - const denom = await this.q().get(Stores.denominations, [coin0.exchangeBaseUrl, coin.denomPub]); + const denom = await this.q().get(Stores.denominations, [ + coin0.exchangeBaseUrl, + coin.denomPub, + ]); if (!denom) { throw Error(`denom not found (${coin.denomPub})`); } @@ -2731,13 +3078,16 @@ export class Wallet { // refreshed normally (and what about incremental refunds?) const refundAmount = Amounts.parseOrThrow(rp.refund_amount); const refundFee = Amounts.parseOrThrow(rp.refund_fee); - const refreshCost = getTotalRefreshCost(denoms, denom, Amounts.sub(refundAmount, refundFee).amount); + const refreshCost = getTotalRefreshCost( + denoms, + denom, + Amounts.sub(refundAmount, refundFee).amount, + ); feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount; } return feeAcc; } - async processTip(tipToken: TipToken): Promise<TipRecord> { const merchantDomain = new URI(tipToken.pickup_url).origin(); const key = tipToken.tip_id + merchantDomain; @@ -2754,7 +3104,6 @@ export class Wallet { } } - private async processTipImpl(tipToken: TipToken): Promise<TipRecord> { console.log("got tip token", tipToken); @@ -2765,17 +3114,25 @@ export class Wallet { throw Error("tipping failed (invalid expiration)"); } - let tipRecord = await this.q().get(Stores.tips, [tipToken.tip_id, merchantDomain]); + let tipRecord = await this.q().get(Stores.tips, [ + tipToken.tip_id, + merchantDomain, + ]); if (tipRecord && tipRecord.pickedUp) { return tipRecord; } const tipAmount = Amounts.parseOrThrow(tipToken.amount); await this.updateExchangeFromUrl(tipToken.exchange_url); - const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(tipToken.exchange_url, tipAmount); - const planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d))); + const denomsForWithdraw = await this.getVerifiedWithdrawDenomList( + tipToken.exchange_url, + tipAmount, + ); + const planchets = await Promise.all( + denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)), + ); const coinPubs: string[] = planchets.map(x => x.coinPub); - const now = (new Date()).getTime(); + const now = new Date().getTime(); tipRecord = { accepted: false, amount: Amounts.parseOrThrow(tipToken.amount), @@ -2792,11 +3149,14 @@ export class Wallet { let merchantResp; - tipRecord = await this.q().putOrGetExisting(Stores.tips, tipRecord, [tipRecord.tipId, merchantDomain]); + tipRecord = await this.q().putOrGetExisting(Stores.tips, tipRecord, [ + tipRecord.tipId, + merchantDomain, + ]); this.notifier.notify(); // Planchets in the form that the merchant expects - const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ + const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => ({ coin_ev: p.coinEv, denom_pub_hash: p.denomPubHash, })); @@ -2839,13 +3199,14 @@ export class Wallet { tipRecord.pickedUp = true; - await this.q().put(Stores.tips, tipRecord).finish(); + await this.q() + .put(Stores.tips, tipRecord) + .finish(); this.notifier.notify(); return tipRecord; } - /** * Start using the coins from a tip. */ @@ -2881,7 +3242,6 @@ export class Wallet { this.notifier.notify(); } - async getTipStatus(tipToken: TipToken): Promise<TipStatus> { const tipId = tipToken.tip_id; const merchantDomain = new URI(tipToken.pickup_url).origin(); @@ -2901,7 +3261,6 @@ export class Wallet { return tipStatus; } - async abortFailedPayment(contractTermsHash: string): Promise<void> { const purchase = await this.q().get(Stores.purchases, contractTermsHash); if (!purchase) { @@ -2929,7 +3288,7 @@ export class Wallet { try { const config = { headers: { "Content-Type": "application/json;charset=UTF-8" }, - timeout: 5000, /* 5 seconds */ + timeout: 5000 /* 5 seconds */, validateStatus: (s: number) => s === 200, }; resp = await axios.post(purchase.contractTerms.pay_url, abortReq, config); @@ -2946,10 +3305,13 @@ export class Wallet { p.abortDone = true; return p; }; - await this.q().mutate(Stores.purchases, purchase.contractTermsHash, markAbortDone); + await this.q().mutate( + Stores.purchases, + purchase.contractTermsHash, + markAbortDone, + ); } - /** * Synchronously get the paid URL for a resource from the plain fulfillment * URL. Returns undefined if the fulfillment URL is not a resource that was @@ -2965,7 +3327,7 @@ export class Wallet { * based on the current system time. */ async collectGarbage() { - const nowMilli = (new Date()).getTime(); + const nowMilli = new Date().getTime(); const nowSec = Math.floor(nowMilli / 1000); const gcReserve = (r: ReserveRecord, n: number) => { @@ -2978,28 +3340,35 @@ export class Wallet { } return false; }; - await this.q().deleteIf(Stores.reserves, gcReserve).finish(); + await this.q() + .deleteIf(Stores.reserves, gcReserve) + .finish(); const gcProposal = (d: ProposalDownloadRecord, n: number) => { // Delete proposal after 60 minutes or 5 minutes before pay deadline, // whatever comes first. - const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000; - const deadlineExpireMilli = nowMilli + (1000 * 60 * 60); + const deadlinePayMilli = + getTalerStampSec(d.contractTerms.pay_deadline)! * 1000; + const deadlineExpireMilli = nowMilli + 1000 * 60 * 60; return d.timestamp < Math.min(deadlinePayMilli, deadlineExpireMilli); }; - await this.q().deleteIf(Stores.proposals, gcProposal).finish(); + await this.q() + .deleteIf(Stores.proposals, gcProposal) + .finish(); const activeExchanges: string[] = []; const gcExchange = (d: ExchangeRecord, n: number) => { // Delete if if unused and last update more than 20 minutes ago - if (!d.lastUsedTime && nowMilli > d.lastUpdateTime + (1000 * 60 * 20)) { + if (!d.lastUsedTime && nowMilli > d.lastUpdateTime + 1000 * 60 * 20) { return true; } activeExchanges.push(d.baseUrl); return false; }; - await this.q().deleteIf(Stores.exchanges, gcExchange).finish(); + await this.q() + .deleteIf(Stores.exchanges, gcExchange) + .finish(); const gcDenominations = (d: DenominationRecord, n: number) => { if (nowSec > getTalerStampSec(d.stampExpireDeposit)!) { @@ -3010,7 +3379,9 @@ export class Wallet { } return false; }; - await this.q().deleteIf(Stores.denominations, gcDenominations).finish(); + await this.q() + .deleteIf(Stores.denominations, gcDenominations) + .finish(); const gcWireFees = (r: ExchangeWireFeesRecord, n: number) => { if (activeExchanges.indexOf(r.exchangeBaseUrl) < 0) { @@ -3018,8 +3389,9 @@ export class Wallet { } return false; }; - await this.q().deleteIf(Stores.exchangeWireFees, gcWireFees).finish(); - + await this.q() + .deleteIf(Stores.exchangeWireFees, gcWireFees) + .finish(); // FIXME(#5210) also GC coins } |