summaryrefslogtreecommitdiff
path: root/src/wallet.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet.ts')
-rw-r--r--src/wallet.ts198
1 files changed, 152 insertions, 46 deletions
diff --git a/src/wallet.ts b/src/wallet.ts
index 68d70b0bb..3d095fc06 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -82,6 +82,8 @@ import {
WalletBalanceEntry,
WireFee,
WireInfo,
+ RefundPermission,
+ PurchaseRecord,
} from "./types";
import URI = require("urijs");
@@ -241,19 +243,6 @@ class WireDetailJson {
}
-interface TransactionRecord {
- contractTermsHash: string;
- contractTerms: ContractTerms;
- payReq: PayReq;
- merchantSig: string;
-
- /**
- * The transaction isn't active anymore, it's either successfully paid
- * or refunded/aborted.
- */
- finished: boolean;
-}
-
/**
* Badge that shows activity for the wallet.
@@ -424,6 +413,8 @@ export function selectPayCoins(cds: CoinWithDenom[], paymentAmount: AmountJson,
denom.feeDeposit).amount) >= 0;
isBelowFee = Amounts.cmp(accFee, depositFeeLimit) <= 0;
+ console.log("coin selection", { coversAmount, isBelowFee, accFee, accAmount, paymentAmount });
+
if ((coversAmount && isBelowFee) || coversAmountWithFee) {
return cdsResult;
}
@@ -516,13 +507,13 @@ export namespace Stores {
}
}
- class TransactionsStore extends Store<TransactionRecord> {
+ class PurchasesStore extends Store<PurchaseRecord> {
constructor() {
- super("transactions", {keyPath: "contractTermsHash"});
+ super("purchases", {keyPath: "contractTermsHash"});
}
- fulfillmentUrlIndex = new Index<string, TransactionRecord>(this, "fulfillment_url", "contractTerms.fulfillment_url");
- orderIdIndex = new Index<string, TransactionRecord>(this, "order_id", "contractTerms.order_id");
+ fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillment_url", "contractTerms.fulfillment_url");
+ orderIdIndex = new Index<string, PurchaseRecord>(this, "order_id", "contractTerms.order_id");
}
class DenominationsStore extends Store<DenominationRecord> {
@@ -566,9 +557,9 @@ export namespace Stores {
export const nonces = new NonceStore();
export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
export const proposals = new ProposalsStore();
- export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "meltCoinPub"});
+ export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
export const reserves = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
- export const transactions = new TransactionsStore();
+ export const purchases = new PurchasesStore();
}
/* tslint:enable:completed-docs */
@@ -770,6 +761,8 @@ export class Wallet {
cds.push({coin, denom});
}
+ console.log("coin return: selecting from possible coins", { cds, amount } );
+
return selectPayCoins(cds, amount, amount);
}
@@ -909,12 +902,14 @@ export class Wallet {
merchant_pub: proposal.contractTerms.merchant_pub,
order_id: proposal.contractTerms.order_id,
};
- const t: TransactionRecord = {
+ const t: PurchaseRecord = {
contractTerms: proposal.contractTerms,
contractTermsHash: proposal.contractTermsHash,
finished: false,
merchantSig: proposal.merchantSig,
payReq,
+ refundsDone: {},
+ refundsPending: {},
};
const historyEntry: HistoryRecord = {
@@ -931,7 +926,7 @@ export class Wallet {
};
await this.q()
- .put(Stores.transactions, t)
+ .put(Stores.purchases, t)
.put(Stores.history, historyEntry)
.putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin))
.finish();
@@ -972,9 +967,9 @@ export class Wallet {
throw Error(`proposal with id ${proposalId} not found`);
}
- const transaction = await this.q().get(Stores.transactions, proposal.contractTermsHash);
+ const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
- if (transaction) {
+ if (purchase) {
// Already payed ...
return "paid";
}
@@ -1017,8 +1012,8 @@ export class Wallet {
}
// First check if we already payed for it.
- const transaction = await this.q().get(Stores.transactions, proposal.contractTermsHash);
- if (transaction) {
+ const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
+ if (purchase) {
return "paid";
}
@@ -1049,7 +1044,7 @@ export class Wallet {
async queryPayment(url: string): Promise<QueryPaymentResult> {
console.log("query for payment", url);
- const t = await this.q().getIndexed(Stores.transactions.fulfillmentUrlIndex, url);
+ const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url);
if (!t) {
console.log("query for payment failed");
@@ -1845,7 +1840,7 @@ export class Wallet {
if (c.suspended) {
return balance;
}
- if (!(c.status === CoinStatus.Dirty || c.status === CoinStatus.Fresh)) {
+ if (!(c.status === CoinStatus.Fresh)) {
return balance;
}
console.log("collecting balance");
@@ -1890,7 +1885,7 @@ export class Wallet {
return balance;
}
- function collectPayments(t: TransactionRecord, balance: WalletBalance) {
+ function collectPayments(t: PurchaseRecord, balance: WalletBalance) {
if (t.finished) {
return balance;
}
@@ -1934,7 +1929,7 @@ export class Wallet {
.reduce(collectPendingWithdraw, balance);
tx.iter(Stores.reserves)
.reduce(collectPaybacks, balance);
- tx.iter(Stores.transactions)
+ tx.iter(Stores.purchases)
.reduce(collectPayments, balance);
await tx.finish();
return balance;
@@ -2008,25 +2003,30 @@ export class Wallet {
// Store refresh session and subtract refreshed amount from
// coin in the same transaction.
- await this.q()
- .put(Stores.refresh, refreshSession)
- .mutate(Stores.coins, coin.coinPub, mutateCoin)
- .finish();
+ const query = this.q();
+ query.put(Stores.refresh, refreshSession, "refreshKey")
+ .mutate(Stores.coins, coin.coinPub, mutateCoin);
+ await query.finish();
+
+ const key = query.key("refreshKey");
+ if (!key || typeof key !== "number") {
+ throw Error("insert failed");
+ }
+
+ refreshSession.id = key;
return refreshSession;
}
async refresh(oldCoinPub: string): Promise<void> {
- let refreshSession: RefreshSessionRecord|undefined;
- const oldSession = await this.q().get(Stores.refresh, oldCoinPub);
- if (oldSession) {
- console.log("got old session for", oldCoinPub);
- console.log(oldSession);
- refreshSession = oldSession;
- } else {
- refreshSession = await this.createRefreshSession(oldCoinPub);
+
+ 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);
}
+ let refreshSession = await this.createRefreshSession(oldCoinPub);
if (!refreshSession) {
// refreshing not necessary
console.log("not refreshing", oldCoinPub);
@@ -2040,9 +2040,8 @@ export class Wallet {
return;
}
if (typeof refreshSession.norevealIndex !== "number") {
- const coinPub = refreshSession.meltCoinPub;
await this.refreshMelt(refreshSession);
- const r = await this.q().get<RefreshSessionRecord>(Stores.refresh, coinPub);
+ const r = await this.q().get<RefreshSessionRecord>(Stores.refresh, refreshSession.id);
if (!r) {
throw Error("refresh session does not exist anymore");
}
@@ -2282,7 +2281,7 @@ export class Wallet {
async paymentSucceeded(contractTermsHash: string, merchantSig: string): Promise<any> {
const doPaymentSucceeded = async() => {
- const t = await this.q().get<TransactionRecord>(Stores.transactions,
+ const t = await this.q().get<PurchaseRecord>(Stores.purchases,
contractTermsHash);
if (!t) {
console.error("contract not found");
@@ -2309,7 +2308,7 @@ export class Wallet {
await this.q()
.putAll(Stores.coins, modifiedCoins)
- .put(Stores.transactions, t)
+ .put(Stores.purchases, t)
.finish();
for (const c of t.payReq.coins) {
this.refresh(c.coin_pub);
@@ -2422,7 +2421,7 @@ export class Wallet {
const senderWiresSet = new Set();
await this.q().iter(Stores.reserves).map((x) => {
if (x.senderWire) {
- senderWiresSet.add(JSON.stringify(x.senderWire));
+ senderWiresSet.add(canonicalJson(x.senderWire));
}
}).run();
const senderWires = Array.from(senderWiresSet).map((x) => JSON.parse(x));
@@ -2450,6 +2449,7 @@ export class Wallet {
console.error(`Exchange ${req.exchange} not known to the wallet`);
return;
}
+ console.log("selecting coins for return:", req);
const cds = await this.getCoinsForReturn(req.exchange, req.amount);
console.log(cds);
@@ -2560,4 +2560,110 @@ export class Wallet {
await this.q().put(Stores.coinsReturns, currentCrr);
}
}
+
+ async acceptRefund(refundPermissions: RefundPermission[]): Promise<void> {
+ if (!refundPermissions.length) {
+ console.warn("got empty refund list");
+ return;
+ }
+ const hc = refundPermissions[0].h_contract_terms;
+ if (!hc) {
+ throw Error("h_contract_terms missing in refund permission");
+ }
+ const m = refundPermissions[0].merchant_pub;
+ if (!hc) {
+ throw Error("merchant_pub missing in refund permission");
+ }
+ for (const perm of refundPermissions) {
+ if (perm.h_contract_terms !== hc) {
+ throw Error("h_contract_terms different in refund permission");
+ }
+ if (perm.merchant_pub !== m) {
+ throw Error("merchant_pub different in refund permission");
+ }
+ }
+
+ /**
+ * Add refund to purchase if not already added.
+ */
+ function f(t: PurchaseRecord|undefined): PurchaseRecord|undefined {
+ if (!t) {
+ console.error("purchase not found, not adding refunds");
+ return;
+ }
+
+ for (const perm of refundPermissions) {
+ if (!t.refundsPending[perm.merchant_sig] && !t.refundsDone[perm.merchant_sig]) {
+ t.refundsPending[perm.merchant_sig] = perm;
+ }
+ }
+ return t;
+ }
+
+ // Add the refund permissions to the purchase within a DB transaction
+ await this.q().mutate(Stores.purchases, hc, f).finish();
+ this.notifier.notify();
+
+ // Start submitting it but don't wait for it here.
+ this.submitRefunds(hc);
+ }
+
+ 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);
+ return;
+ }
+ const pendingKeys = Object.keys(purchase.refundsPending);
+ if (pendingKeys.length === 0) {
+ return;
+ }
+ for (const pk of pendingKeys) {
+ const perm = purchase.refundsPending[pk];
+ console.log("sending refund permission", perm);
+ const reqUrl = (new URI("refund")).absoluteTo(purchase.payReq.exchange);
+ const resp = await this.http.postJson(reqUrl.href(), perm);
+ if (resp.status !== 200) {
+ console.error("refund failed", resp);
+ continue;
+ }
+
+ // Transactionally mark successful refunds as done
+ const transformPurchase = (t: PurchaseRecord|undefined): PurchaseRecord|undefined => {
+ if (!t) {
+ console.warn("purchase not found, not updating refund");
+ return;
+ }
+ if (t.refundsPending[pk]) {
+ t.refundsDone[pk] = t.refundsPending[pk];
+ delete t.refundsPending[pk];
+ }
+ return t;
+ };
+ const transformCoin = (c: CoinRecord|undefined): CoinRecord|undefined => {
+ if (!c) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ c.status = CoinStatus.Dirty;
+ c.currentAmount = Amounts.add(c.currentAmount, perm.refund_amount).amount;
+ c.currentAmount = Amounts.sub(c.currentAmount, perm.refund_fee).amount;
+
+ return c;
+ };
+
+
+ await this.q()
+ .mutate(Stores.purchases, contractTermsHash, transformPurchase)
+ .mutate(Stores.coins, perm.coin_pub, transformCoin)
+ .finish();
+ this.refresh(perm.coin_pub);
+ }
+
+ this.notifier.notify();
+ }
+
+ async getPurchase(contractTermsHash: string): Promise<PurchaseRecord|undefined> {
+ return this.q().get(Stores.purchases, contractTermsHash);
+ }
}