diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-12-12 21:54:14 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-12-12 21:54:14 +0100 |
commit | ca2a46a8575d66d529accb1ce3aaf97be8f37e2f (patch) | |
tree | c05c3452c2dfe81713cf36ce06d205faa6bfb0fa /src | |
parent | 659435570440c5a5eacde3a2e6ef5b3f3430a45f (diff) |
precompute speculative signature for payment
Diffstat (limited to 'src')
-rw-r--r-- | src/crypto/cryptoApi.ts | 4 | ||||
-rw-r--r-- | src/crypto/cryptoWorker.ts | 11 | ||||
-rw-r--r-- | src/query.ts | 32 | ||||
-rw-r--r-- | src/types.ts | 8 | ||||
-rw-r--r-- | src/wallet.ts | 71 |
5 files changed, 112 insertions, 14 deletions
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index 94083d622..d0ba6ada8 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -275,8 +275,8 @@ export class CryptoApi { return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub); } - isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) { - return this.doRpc<PayCoinInfo>("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, diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 92947d039..28634b234 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -261,7 +261,11 @@ namespace RpcFunctions { */ export function signDeposit(contractTerms: ContractTerms, cds: CoinWithDenom[]): PayCoinInfo { - const ret: PayCoinInfo = []; + const ret: PayCoinInfo = { + originalCoins: [], + updatedCoins: [], + sigs: [], + }; const contractTermsHash = hashString(canonicalJson(contractTerms)); @@ -275,6 +279,7 @@ namespace RpcFunctions { const amountRemaining = new native.Amount(total); for (const cd of cds) { let coinSpend: Amount; + const originalCoin = { ...(cd.coin) }; if (amountRemaining.value === 0 && amountRemaining.fraction === 0) { break; @@ -324,7 +329,9 @@ namespace RpcFunctions { f: coinSpend.toJson(), ub_sig: cd.coin.denomSig, }; - ret.push({sig: s, updatedCoin: cd.coin}); + ret.sigs.push(s); + ret.updatedCoins.push(cd.coin); + ret.originalCoins.push(originalCoin); } return ret; } diff --git a/src/query.ts b/src/query.ts index 9889ed13e..b199e0e9c 100644 --- a/src/query.ts +++ b/src/query.ts @@ -807,6 +807,38 @@ export class QueryRoot { } /** + * Get get objects from a store by their keys. + * If no object for a key exists, the resulting position in the array + * contains 'undefined'. + */ + getMany<T>(store: Store<T>, keys: any[]): Promise<T[]> { + this.checkFinished(); + + const { resolve, promise } = openPromise(); + const results: T[] = []; + + const doGetMany = (tx: IDBTransaction) => { + for (const key of keys) { + if (key === void 0) { + throw Error("key must not be undefined"); + } + const req = tx.objectStore(store.name).get(key); + req.onsuccess = () => { + results.push(req.result); + if (results.length == keys.length) { + resolve(results); + } + }; + } + }; + + this.addWork(doGetMany, store.name, false); + return Promise.resolve() + .then(() => this.finish()) + .then(() => promise); + } + + /** * Get one object from a store by its key. */ getIndexed<I extends IDBValidKey, T>(index: Index<I, T>, diff --git a/src/types.ts b/src/types.ts index ca01203d5..c0f36fc98 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1297,7 +1297,11 @@ export interface ExchangeWireFeesRecord { * Coins used for a payment, with signatures authorizing the payment and the * coins with remaining value updated to accomodate for a payment. */ -export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>; +export interface PayCoinInfo { + originalCoins: CoinRecord[]; + updatedCoins: CoinRecord[]; + sigs: CoinPaySig[]; +} /** @@ -1787,8 +1791,6 @@ export interface PurchaseRecord { * Set to 0 if no refund was made on the purchase. */ timestamp_refund: number; - - userAccepted: boolean; } diff --git a/src/wallet.ts b/src/wallet.ts index 2ab24571c..08e3049c5 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -324,6 +324,13 @@ export interface CoinsReturnRecord { wire: any; } +interface SpeculativePayData { + payCoinInfo: PayCoinInfo; + exchangeUrl: string; + proposalId: number; + proposal: ProposalRecord; +} + /** * Wallet protocol version spoken with the exchange @@ -651,6 +658,7 @@ export class Wallet { private processPreCoinConcurrent = 0; private processPreCoinThrottle: {[url: string]: number} = {}; private timerGroup: TimerGroup; + private speculativePayData: SpeculativePayData | undefined; /** * Set of identifiers for running operations. @@ -971,7 +979,7 @@ export class Wallet { payCoinInfo: PayCoinInfo, chosenExchange: string): Promise<void> { const payReq: PayReq = { - coins: payCoinInfo.map((x) => x.sig), + coins: payCoinInfo.sigs, exchange: chosenExchange, merchant_pub: proposal.contractTerms.merchant_pub, order_id: proposal.contractTerms.order_id, @@ -990,7 +998,7 @@ export class Wallet { await this.q() .put(Stores.purchases, t) - .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) + .putAll(Stores.coins, payCoinInfo.updatedCoins) .finish(); this.badge.showNotification(); this.notifier.notify(); @@ -1048,17 +1056,53 @@ export class Wallet { console.log("not confirming payment, insufficient coins"); return "insufficient-balance"; } - const {exchangeUrl, cds} = res; - const ds = await this.cryptoApi.signDeposit(proposal.contractTerms, cds); - await this.recordConfirmPay(proposal, ds, exchangeUrl); + const sd = await this.getSpeculativePayData(proposalId); + if (!sd) { + const { exchangeUrl, cds } = res; + const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds); + await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl); + } else { + await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl); + } + return "paid"; } + /** + * Get the speculative pay data, but only if coins have not changed in between. + */ + async getSpeculativePayData(proposalId: number): Promise<SpeculativePayData | undefined> { + const sp = this.speculativePayData; + if (!sp) { + return; + } + if (sp.proposalId != proposalId) { + return; + } + const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub); + const coins = await this.q().getMany(Stores.coins, coinKeys); + for (let i = 0; i < coins.length; i++) { + const specCoin = sp.payCoinInfo.originalCoins[i]; + const currentCoin = coins[i]; + + // Coin does not exist anymore! + if (!currentCoin) { + return; + } + 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. + * + * Also speculatively computes the signature for the payment to make the payment + * look faster to the user. */ async checkPay(proposalId: number): Promise<CheckPayResult> { const proposal = await this.q().get(Stores.proposals, proposalId); @@ -1089,6 +1133,19 @@ export class Wallet { console.log("not confirming payment, insufficient coins"); return { status: "insufficient-balance" }; } + + // Only create speculative signature if we don't already have one for this proposal + if ((!this.speculativePayData) || (this.speculativePayData && this.speculativePayData.proposalId != proposalId)) { + const { exchangeUrl, cds } = res; + const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds); + this.speculativePayData = { + exchangeUrl, + payCoinInfo, + proposal, + proposalId, + }; + } + return { status: "payment-possible", coinSelection: res }; } @@ -2673,7 +2730,7 @@ export class Wallet { console.log("pci", payCoinInfo); - const coins = payCoinInfo.map((pci) => ({ coinPaySig: pci.sig })); + const coins = payCoinInfo.sigs.map((s) => ({ coinPaySig: s })); const coinsReturnRecord: CoinsReturnRecord = { coins, @@ -2686,7 +2743,7 @@ export class Wallet { await this.q() .put(Stores.coinsReturns, coinsReturnRecord) - .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) + .putAll(Stores.coins, payCoinInfo.updatedCoins) .finish(); this.badge.showNotification(); this.notifier.notify(); |