aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2017-12-12 21:54:14 +0100
committerFlorian Dold <florian.dold@gmail.com>2017-12-12 21:54:14 +0100
commitca2a46a8575d66d529accb1ce3aaf97be8f37e2f (patch)
treec05c3452c2dfe81713cf36ce06d205faa6bfb0fa
parent659435570440c5a5eacde3a2e6ef5b3f3430a45f (diff)
precompute speculative signature for payment
-rw-r--r--src/crypto/cryptoApi.ts4
-rw-r--r--src/crypto/cryptoWorker.ts11
-rw-r--r--src/query.ts32
-rw-r--r--src/types.ts8
-rw-r--r--src/wallet.ts71
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();