pending operations (pay/proposals)
This commit is contained in:
parent
a5137c3265
commit
c33dd75711
@ -200,9 +200,11 @@ export function installAndroidWalletListener() {
|
|||||||
const wallet = await wp.promise;
|
const wallet = await wp.promise;
|
||||||
wallet.stop();
|
wallet.stop();
|
||||||
wp = openPromise<Wallet>();
|
wp = openPromise<Wallet>();
|
||||||
if (walletArgs && walletArgs.persistentStoragePath) {
|
const oldArgs = walletArgs;
|
||||||
|
walletArgs = { ...oldArgs };
|
||||||
|
if (oldArgs && oldArgs.persistentStoragePath) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(walletArgs.persistentStoragePath);
|
fs.unlinkSync(oldArgs.persistentStoragePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error while deleting the wallet db:", e);
|
console.error("Error while deleting the wallet db:", e);
|
||||||
}
|
}
|
||||||
@ -210,6 +212,12 @@ export function installAndroidWalletListener() {
|
|||||||
walletArgs.persistentStoragePath = undefined;
|
walletArgs.persistentStoragePath = undefined;
|
||||||
}
|
}
|
||||||
maybeWallet = undefined;
|
maybeWallet = undefined;
|
||||||
|
const w = await getDefaultNodeWallet(walletArgs);
|
||||||
|
maybeWallet = w;
|
||||||
|
w.runLoopScheduledRetries().catch((e) => {
|
||||||
|
console.error("Error during wallet retry loop", e);
|
||||||
|
});
|
||||||
|
wp.resolve(w);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -586,9 +586,47 @@ export interface CoinRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ProposalStatus {
|
export enum ProposalStatus {
|
||||||
|
/**
|
||||||
|
* Not downloaded yet.
|
||||||
|
*/
|
||||||
|
DOWNLOADING = "downloading",
|
||||||
|
/**
|
||||||
|
* Proposal downloaded, but the user needs to accept/reject it.
|
||||||
|
*/
|
||||||
PROPOSED = "proposed",
|
PROPOSED = "proposed",
|
||||||
|
/**
|
||||||
|
* The user has accepted the proposal.
|
||||||
|
*/
|
||||||
ACCEPTED = "accepted",
|
ACCEPTED = "accepted",
|
||||||
|
/**
|
||||||
|
* The user has rejected the proposal.
|
||||||
|
*/
|
||||||
REJECTED = "rejected",
|
REJECTED = "rejected",
|
||||||
|
/**
|
||||||
|
* Downloaded proposal was detected as a re-purchase.
|
||||||
|
*/
|
||||||
|
REPURCHASE = "repurchase",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Checkable.Class()
|
||||||
|
export class ProposalDownload {
|
||||||
|
/**
|
||||||
|
* The contract that was offered by the merchant.
|
||||||
|
*/
|
||||||
|
@Checkable.Value(() => ContractTerms)
|
||||||
|
contractTerms: ContractTerms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature by the merchant over the contract details.
|
||||||
|
*/
|
||||||
|
@Checkable.String()
|
||||||
|
merchantSig: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature by the merchant over the contract details.
|
||||||
|
*/
|
||||||
|
@Checkable.String()
|
||||||
|
contractTermsHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -603,22 +641,9 @@ export class ProposalRecord {
|
|||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract that was offered by the merchant.
|
* Downloaded data from the merchant.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => ContractTerms)
|
download: ProposalDownload | undefined;
|
||||||
contractTerms: ContractTerms;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signature by the merchant over the contract details.
|
|
||||||
*/
|
|
||||||
@Checkable.String()
|
|
||||||
merchantSig: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the contract terms.
|
|
||||||
*/
|
|
||||||
@Checkable.String()
|
|
||||||
contractTermsHash: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique ID when the order is stored in the wallet DB.
|
* Unique ID when the order is stored in the wallet DB.
|
||||||
@ -639,9 +664,18 @@ export class ProposalRecord {
|
|||||||
@Checkable.String()
|
@Checkable.String()
|
||||||
noncePriv: string;
|
noncePriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key for the nonce.
|
||||||
|
*/
|
||||||
|
@Checkable.String()
|
||||||
|
noncePub: string;
|
||||||
|
|
||||||
@Checkable.String()
|
@Checkable.String()
|
||||||
proposalStatus: ProposalStatus;
|
proposalStatus: ProposalStatus;
|
||||||
|
|
||||||
|
@Checkable.String()
|
||||||
|
repurchaseProposalId: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session ID we got when downloading the contract.
|
* Session ID we got when downloading the contract.
|
||||||
*/
|
*/
|
||||||
@ -911,6 +945,12 @@ export interface PurchaseRecord {
|
|||||||
* The abort (with refund) was completed for this (incomplete!) purchase.
|
* The abort (with refund) was completed for this (incomplete!) purchase.
|
||||||
*/
|
*/
|
||||||
abortDone: boolean;
|
abortDone: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proposal ID for this purchase. Uniquely identifies the
|
||||||
|
* purchase and the proposal.
|
||||||
|
*/
|
||||||
|
proposalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1076,7 +1116,7 @@ export namespace Stores {
|
|||||||
|
|
||||||
class PurchasesStore extends Store<PurchaseRecord> {
|
class PurchasesStore extends Store<PurchaseRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("purchases", { keyPath: "contractTermsHash" });
|
super("purchases", { keyPath: "proposalId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
*/
|
*/
|
||||||
import { openPromise } from "./promiseUtils";
|
import { openPromise } from "./promiseUtils";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of an inner join.
|
* Result of an inner join.
|
||||||
*/
|
*/
|
||||||
@ -67,7 +66,7 @@ export interface IndexOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requestToPromise(req: IDBRequest): Promise<any> {
|
function requestToPromise(req: IDBRequest): Promise<any> {
|
||||||
const stack = Error("Failed request was started here.")
|
const stack = Error("Failed request was started here.");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
resolve(req.result);
|
resolve(req.result);
|
||||||
@ -103,7 +102,7 @@ export async function oneShotGet<T>(
|
|||||||
): Promise<T | undefined> {
|
): Promise<T | undefined> {
|
||||||
const tx = db.transaction([store.name], "readonly");
|
const tx = db.transaction([store.name], "readonly");
|
||||||
const req = tx.objectStore(store.name).get(key);
|
const req = tx.objectStore(store.name).get(key);
|
||||||
const v = await requestToPromise(req)
|
const v = await requestToPromise(req);
|
||||||
await transactionToPromise(tx);
|
await transactionToPromise(tx);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@ -335,6 +334,17 @@ class TransactionHandle {
|
|||||||
return requestToPromise(req);
|
return requestToPromise(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIndexed<S extends IDBValidKey, T>(
|
||||||
|
index: Index<S, T>,
|
||||||
|
key: any,
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
const req = this.tx
|
||||||
|
.objectStore(index.storeName)
|
||||||
|
.index(index.indexName)
|
||||||
|
.get(key);
|
||||||
|
return requestToPromise(req);
|
||||||
|
}
|
||||||
|
|
||||||
iter<T>(store: Store<T>, key?: any): ResultStream<T> {
|
iter<T>(store: Store<T>, key?: any): ResultStream<T> {
|
||||||
const req = this.tx.objectStore(store.name).openCursor(key);
|
const req = this.tx.objectStore(store.name).openCursor(key);
|
||||||
return new ResultStream<T>(req);
|
return new ResultStream<T>(req);
|
||||||
@ -407,18 +417,20 @@ function runWithTransaction<T>(
|
|||||||
};
|
};
|
||||||
const th = new TransactionHandle(tx);
|
const th = new TransactionHandle(tx);
|
||||||
const resP = f(th);
|
const resP = f(th);
|
||||||
resP.then(result => {
|
resP
|
||||||
gotFunResult = true;
|
.then(result => {
|
||||||
funResult = result;
|
gotFunResult = true;
|
||||||
}).catch((e) => {
|
funResult = result;
|
||||||
if (e == TransactionAbort) {
|
})
|
||||||
console.info("aborting transaction");
|
.catch(e => {
|
||||||
} else {
|
if (e == TransactionAbort) {
|
||||||
tx.abort();
|
console.info("aborting transaction");
|
||||||
console.error("Transaction failed:", e);
|
} else {
|
||||||
console.error(stack);
|
tx.abort();
|
||||||
}
|
console.error("Transaction failed:", e);
|
||||||
});
|
console.error(stack);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ export async function getHistory(
|
|||||||
// This works as timestamps are guaranteed to be monotonically
|
// This works as timestamps are guaranteed to be monotonically
|
||||||
// increasing even
|
// increasing even
|
||||||
|
|
||||||
|
/*
|
||||||
const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
|
const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
|
||||||
for (const p of proposals) {
|
for (const p of proposals) {
|
||||||
history.push({
|
history.push({
|
||||||
@ -51,6 +52,7 @@ export async function getHistory(
|
|||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const withdrawals = await oneShotIter(
|
const withdrawals = await oneShotIter(
|
||||||
ws.db,
|
ws.db,
|
||||||
|
@ -55,6 +55,7 @@ import {
|
|||||||
strcmp,
|
strcmp,
|
||||||
extractTalerStamp,
|
extractTalerStamp,
|
||||||
canonicalJson,
|
canonicalJson,
|
||||||
|
extractTalerStampOrThrow,
|
||||||
} from "../util/helpers";
|
} from "../util/helpers";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
@ -320,31 +321,41 @@ async function recordConfirmPay(
|
|||||||
payCoinInfo: PayCoinInfo,
|
payCoinInfo: PayCoinInfo,
|
||||||
chosenExchange: string,
|
chosenExchange: string,
|
||||||
): Promise<PurchaseRecord> {
|
): Promise<PurchaseRecord> {
|
||||||
|
const d = proposal.download;
|
||||||
|
if (!d) {
|
||||||
|
throw Error("proposal is in invalid state");
|
||||||
|
}
|
||||||
const payReq: PayReq = {
|
const payReq: PayReq = {
|
||||||
coins: payCoinInfo.sigs,
|
coins: payCoinInfo.sigs,
|
||||||
merchant_pub: proposal.contractTerms.merchant_pub,
|
merchant_pub: d.contractTerms.merchant_pub,
|
||||||
mode: "pay",
|
mode: "pay",
|
||||||
order_id: proposal.contractTerms.order_id,
|
order_id: d.contractTerms.order_id,
|
||||||
};
|
};
|
||||||
const t: PurchaseRecord = {
|
const t: PurchaseRecord = {
|
||||||
abortDone: false,
|
abortDone: false,
|
||||||
abortRequested: false,
|
abortRequested: false,
|
||||||
contractTerms: proposal.contractTerms,
|
contractTerms: d.contractTerms,
|
||||||
contractTermsHash: proposal.contractTermsHash,
|
contractTermsHash: d.contractTermsHash,
|
||||||
finished: false,
|
finished: false,
|
||||||
lastSessionId: undefined,
|
lastSessionId: undefined,
|
||||||
merchantSig: proposal.merchantSig,
|
merchantSig: d.merchantSig,
|
||||||
payReq,
|
payReq,
|
||||||
refundsDone: {},
|
refundsDone: {},
|
||||||
refundsPending: {},
|
refundsPending: {},
|
||||||
timestamp: getTimestampNow(),
|
timestamp: getTimestampNow(),
|
||||||
timestamp_refund: undefined,
|
timestamp_refund: undefined,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
};
|
};
|
||||||
|
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
ws.db,
|
ws.db,
|
||||||
[Stores.coins, Stores.purchases],
|
[Stores.coins, Stores.purchases, Stores.proposals],
|
||||||
async tx => {
|
async tx => {
|
||||||
|
const p = await tx.get(Stores.proposals, proposal.proposalId);
|
||||||
|
if (p) {
|
||||||
|
p.proposalStatus = ProposalStatus.ACCEPTED;
|
||||||
|
await tx.put(Stores.proposals, p);
|
||||||
|
}
|
||||||
await tx.put(Stores.purchases, t);
|
await tx.put(Stores.purchases, t);
|
||||||
for (let c of payCoinInfo.updatedCoins) {
|
for (let c of payCoinInfo.updatedCoins) {
|
||||||
await tx.put(Stores.coins, c);
|
await tx.put(Stores.coins, c);
|
||||||
@ -360,7 +371,7 @@ async function recordConfirmPay(
|
|||||||
function getNextUrl(contractTerms: ContractTerms): string {
|
function getNextUrl(contractTerms: ContractTerms): string {
|
||||||
const f = contractTerms.fulfillment_url;
|
const f = contractTerms.fulfillment_url;
|
||||||
if (f.startsWith("http://") || f.startsWith("https://")) {
|
if (f.startsWith("http://") || f.startsWith("https://")) {
|
||||||
const fu = new URL(contractTerms.fulfillment_url)
|
const fu = new URL(contractTerms.fulfillment_url);
|
||||||
fu.searchParams.set("order_id", contractTerms.order_id);
|
fu.searchParams.set("order_id", contractTerms.order_id);
|
||||||
return fu.href;
|
return fu.href;
|
||||||
} else {
|
} else {
|
||||||
@ -370,9 +381,9 @@ function getNextUrl(contractTerms: ContractTerms): string {
|
|||||||
|
|
||||||
export async function abortFailedPayment(
|
export async function abortFailedPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
contractTermsHash: string,
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("Purchase not found, unable to abort with refund");
|
throw Error("Purchase not found, unable to abort with refund");
|
||||||
}
|
}
|
||||||
@ -409,7 +420,7 @@ export async function abortFailedPayment(
|
|||||||
await acceptRefundResponse(ws, refundResponse);
|
await acceptRefundResponse(ws, refundResponse);
|
||||||
|
|
||||||
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
||||||
const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -418,30 +429,19 @@ export async function abortFailedPayment(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function processDownloadProposal(
|
||||||
* Download a proposal and store it in the database.
|
|
||||||
* Returns an id for it to retrieve it later.
|
|
||||||
*
|
|
||||||
* @param sessionId Current session ID, if the proposal is being
|
|
||||||
* downloaded in the context of a session ID.
|
|
||||||
*/
|
|
||||||
async function downloadProposal(
|
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
url: string,
|
proposalId: string,
|
||||||
sessionId?: string,
|
): Promise<void> {
|
||||||
): Promise<string> {
|
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||||
const oldProposal = await oneShotGetIndexed(
|
if (!proposal) {
|
||||||
ws.db,
|
return;
|
||||||
Stores.proposals.urlIndex,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
if (oldProposal) {
|
|
||||||
return oldProposal.proposalId;
|
|
||||||
}
|
}
|
||||||
|
if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
|
||||||
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
|
return;
|
||||||
const parsed_url = new URL(url);
|
}
|
||||||
parsed_url.searchParams.set("nonce", pub);
|
const parsed_url = new URL(proposal.url);
|
||||||
|
parsed_url.searchParams.set("nonce", proposal.noncePub);
|
||||||
const urlWithNonce = parsed_url.href;
|
const urlWithNonce = parsed_url.href;
|
||||||
console.log("downloading contract from '" + urlWithNonce + "'");
|
console.log("downloading contract from '" + urlWithNonce + "'");
|
||||||
let resp;
|
let resp;
|
||||||
@ -452,39 +452,103 @@ async function downloadProposal(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proposal = Proposal.checked(resp.responseJson);
|
const proposalResp = Proposal.checked(resp.responseJson);
|
||||||
|
|
||||||
const contractTermsHash = await ws.cryptoApi.hashString(
|
const contractTermsHash = await ws.cryptoApi.hashString(
|
||||||
canonicalJson(proposal.contract_terms),
|
canonicalJson(proposalResp.contract_terms),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
|
||||||
|
|
||||||
|
await runWithWriteTransaction(
|
||||||
|
ws.db,
|
||||||
|
[Stores.proposals, Stores.purchases],
|
||||||
|
async tx => {
|
||||||
|
const p = await tx.get(Stores.proposals, proposalId);
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fulfillmentUrl.startsWith("http://") ||
|
||||||
|
fulfillmentUrl.startsWith("https://")
|
||||||
|
) {
|
||||||
|
const differentPurchase = await tx.getIndexed(
|
||||||
|
Stores.purchases.fulfillmentUrlIndex,
|
||||||
|
fulfillmentUrl,
|
||||||
|
);
|
||||||
|
if (differentPurchase) {
|
||||||
|
p.proposalStatus = ProposalStatus.REPURCHASE;
|
||||||
|
p.repurchaseProposalId = differentPurchase.proposalId;
|
||||||
|
await tx.put(Stores.proposals, p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.download = {
|
||||||
|
contractTerms: proposalResp.contract_terms,
|
||||||
|
merchantSig: proposalResp.sig,
|
||||||
|
contractTermsHash,
|
||||||
|
};
|
||||||
|
p.proposalStatus = ProposalStatus.PROPOSED;
|
||||||
|
await tx.put(Stores.proposals, p);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.notifier.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a proposal and store it in the database.
|
||||||
|
* Returns an id for it to retrieve it later.
|
||||||
|
*
|
||||||
|
* @param sessionId Current session ID, if the proposal is being
|
||||||
|
* downloaded in the context of a session ID.
|
||||||
|
*/
|
||||||
|
async function startDownloadProposal(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
url: string,
|
||||||
|
sessionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const oldProposal = await oneShotGetIndexed(
|
||||||
|
ws.db,
|
||||||
|
Stores.proposals.urlIndex,
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
if (oldProposal) {
|
||||||
|
await processDownloadProposal(ws, oldProposal.proposalId);
|
||||||
|
return oldProposal.proposalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
|
||||||
const proposalId = encodeCrock(getRandomBytes(32));
|
const proposalId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
const proposalRecord: ProposalRecord = {
|
const proposalRecord: ProposalRecord = {
|
||||||
contractTerms: proposal.contract_terms,
|
download: undefined,
|
||||||
contractTermsHash,
|
|
||||||
merchantSig: proposal.sig,
|
|
||||||
noncePriv: priv,
|
noncePriv: priv,
|
||||||
|
noncePub: pub,
|
||||||
timestamp: getTimestampNow(),
|
timestamp: getTimestampNow(),
|
||||||
url,
|
url,
|
||||||
downloadSessionId: sessionId,
|
downloadSessionId: sessionId,
|
||||||
proposalId: proposalId,
|
proposalId: proposalId,
|
||||||
proposalStatus: ProposalStatus.PROPOSED,
|
proposalStatus: ProposalStatus.DOWNLOADING,
|
||||||
|
repurchaseProposalId: undefined,
|
||||||
};
|
};
|
||||||
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
|
|
||||||
ws.notifier.notify();
|
|
||||||
|
|
||||||
|
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
|
||||||
|
await processDownloadProposal(ws, proposalId);
|
||||||
return proposalId;
|
return proposalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitPay(
|
export async function submitPay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
contractTermsHash: string,
|
proposalId: string,
|
||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
): Promise<ConfirmPayResult> {
|
): Promise<ConfirmPayResult> {
|
||||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("Purchase not found: " + contractTermsHash);
|
throw Error("Purchase not found: " + proposalId);
|
||||||
}
|
}
|
||||||
if (purchase.abortRequested) {
|
if (purchase.abortRequested) {
|
||||||
throw Error("not submitting payment for aborted purchase");
|
throw Error("not submitting payment for aborted purchase");
|
||||||
@ -507,7 +571,7 @@ async function submitPay(
|
|||||||
const merchantPub = purchase.contractTerms.merchant_pub;
|
const merchantPub = purchase.contractTerms.merchant_pub;
|
||||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||||
merchantResp.sig,
|
merchantResp.sig,
|
||||||
contractTermsHash,
|
purchase.contractTermsHash,
|
||||||
merchantPub,
|
merchantPub,
|
||||||
);
|
);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
@ -532,14 +596,16 @@ async function submitPay(
|
|||||||
[Stores.coins, Stores.purchases],
|
[Stores.coins, Stores.purchases],
|
||||||
async tx => {
|
async tx => {
|
||||||
for (let c of modifiedCoins) {
|
for (let c of modifiedCoins) {
|
||||||
tx.put(Stores.coins, c);
|
await tx.put(Stores.coins, c);
|
||||||
}
|
}
|
||||||
tx.put(Stores.purchases, purchase);
|
await tx.put(Stores.purchases, purchase);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const c of purchase.payReq.coins) {
|
for (const c of purchase.payReq.coins) {
|
||||||
refresh(ws, c.coin_pub);
|
refresh(ws, c.coin_pub).catch(e => {
|
||||||
|
console.log("error in refreshing after payment:", e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextUrl = getNextUrl(purchase.contractTerms);
|
const nextUrl = getNextUrl(purchase.contractTerms);
|
||||||
@ -570,100 +636,67 @@ export async function preparePay(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposalId: string;
|
const proposalId = await startDownloadProposal(
|
||||||
try {
|
ws,
|
||||||
proposalId = await downloadProposal(
|
uriResult.downloadUrl,
|
||||||
ws,
|
uriResult.sessionId,
|
||||||
uriResult.downloadUrl,
|
);
|
||||||
uriResult.sessionId,
|
|
||||||
);
|
let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
status: "error",
|
|
||||||
error: e.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
|
||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
throw Error(`could not get proposal ${proposalId}`);
|
throw Error(`could not get proposal ${proposalId}`);
|
||||||
}
|
}
|
||||||
|
if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
|
||||||
|
const existingProposalId = proposal.repurchaseProposalId;
|
||||||
|
if (!existingProposalId) {
|
||||||
|
throw Error("invalid proposal state");
|
||||||
|
}
|
||||||
|
proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
throw Error("existing proposal is in wrong state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const d = proposal.download;
|
||||||
|
if (!d) {
|
||||||
|
console.error("bad proposal", proposal);
|
||||||
|
throw Error("proposal is in invalid state");
|
||||||
|
}
|
||||||
|
const contractTerms = d.contractTerms;
|
||||||
|
const merchantSig = d.merchantSig;
|
||||||
|
if (!contractTerms || !merchantSig) {
|
||||||
|
throw Error("BUG: proposal is in invalid state");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("proposal", proposal);
|
console.log("proposal", proposal);
|
||||||
|
|
||||||
const differentPurchase = await oneShotGetIndexed(
|
|
||||||
ws.db,
|
|
||||||
Stores.purchases.fulfillmentUrlIndex,
|
|
||||||
proposal.contractTerms.fulfillment_url,
|
|
||||||
);
|
|
||||||
|
|
||||||
let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
|
|
||||||
let doublePurchaseDetection = false;
|
|
||||||
if (fulfillmentUrl.startsWith("http")) {
|
|
||||||
doublePurchaseDetection = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (differentPurchase && doublePurchaseDetection) {
|
|
||||||
// We do this check to prevent merchant B to find out if we bought a
|
|
||||||
// digital product with merchant A by abusing the existing payment
|
|
||||||
// redirect feature.
|
|
||||||
if (
|
|
||||||
differentPurchase.contractTerms.merchant_pub !=
|
|
||||||
proposal.contractTerms.merchant_pub
|
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
"merchant with different public key offered contract with same fulfillment URL as an existing purchase",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (uriResult.sessionId) {
|
|
||||||
await submitPay(
|
|
||||||
ws,
|
|
||||||
differentPurchase.contractTermsHash,
|
|
||||||
uriResult.sessionId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: "paid",
|
|
||||||
contractTerms: differentPurchase.contractTerms,
|
|
||||||
nextUrl: getNextUrl(differentPurchase.contractTerms),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check if we already payed for it.
|
// First check if we already payed for it.
|
||||||
const purchase = await oneShotGet(
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
ws.db,
|
|
||||||
Stores.purchases,
|
|
||||||
proposal.contractTermsHash,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
|
const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
|
||||||
let wireFeeLimit;
|
let wireFeeLimit;
|
||||||
if (proposal.contractTerms.max_wire_fee) {
|
if (contractTerms.max_wire_fee) {
|
||||||
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
|
||||||
} else {
|
} else {
|
||||||
wireFeeLimit = Amounts.getZero(paymentAmount.currency);
|
wireFeeLimit = Amounts.getZero(paymentAmount.currency);
|
||||||
}
|
}
|
||||||
// If not already payed, check if we could pay for it.
|
// If not already payed, check if we could pay for it.
|
||||||
const res = await getCoinsForPayment(ws, {
|
const res = await getCoinsForPayment(ws, {
|
||||||
allowedAuditors: proposal.contractTerms.auditors,
|
allowedAuditors: contractTerms.auditors,
|
||||||
allowedExchanges: proposal.contractTerms.exchanges,
|
allowedExchanges: contractTerms.exchanges,
|
||||||
depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
|
depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
|
||||||
paymentAmount,
|
paymentAmount,
|
||||||
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
|
wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
|
||||||
wireFeeLimit,
|
wireFeeLimit,
|
||||||
// FIXME: parse this properly
|
wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
|
||||||
wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
|
wireMethod: contractTerms.wire_method,
|
||||||
t_ms: 0,
|
|
||||||
},
|
|
||||||
wireMethod: proposal.contractTerms.wire_method,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
console.log("not confirming payment, insufficient coins");
|
console.log("not confirming payment, insufficient coins");
|
||||||
return {
|
return {
|
||||||
status: "insufficient-balance",
|
status: "insufficient-balance",
|
||||||
contractTerms: proposal.contractTerms,
|
contractTerms: contractTerms,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -676,7 +709,7 @@ export async function preparePay(
|
|||||||
) {
|
) {
|
||||||
const { exchangeUrl, cds, totalAmount } = res;
|
const { exchangeUrl, cds, totalAmount } = res;
|
||||||
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
||||||
proposal.contractTerms,
|
contractTerms,
|
||||||
cds,
|
cds,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
);
|
);
|
||||||
@ -691,19 +724,19 @@ export async function preparePay(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: "payment-possible",
|
status: "payment-possible",
|
||||||
contractTerms: proposal.contractTerms,
|
contractTerms: contractTerms,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
totalFees: res.totalFees,
|
totalFees: res.totalFees,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uriResult.sessionId) {
|
if (uriResult.sessionId) {
|
||||||
await submitPay(ws, purchase.contractTermsHash, uriResult.sessionId);
|
await submitPay(ws, proposalId, uriResult.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "paid",
|
status: "paid",
|
||||||
contractTerms: proposal.contractTerms,
|
contractTerms: purchase.contractTerms,
|
||||||
nextUrl: getNextUrl(purchase.contractTerms),
|
nextUrl: getNextUrl(purchase.contractTerms),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -762,39 +795,37 @@ export async function confirmPay(
|
|||||||
throw Error(`proposal with id ${proposalId} not found`);
|
throw Error(`proposal with id ${proposalId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = sessionIdOverride || proposal.downloadSessionId;
|
const d = proposal.download;
|
||||||
|
if (!d) {
|
||||||
let purchase = await oneShotGet(
|
throw Error("proposal is in invalid state");
|
||||||
ws.db,
|
|
||||||
Stores.purchases,
|
|
||||||
proposal.contractTermsHash,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (purchase) {
|
|
||||||
return submitPay(ws, purchase.contractTermsHash, sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
|
const sessionId = sessionIdOverride || proposal.downloadSessionId;
|
||||||
|
|
||||||
|
let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash);
|
||||||
|
|
||||||
|
if (purchase) {
|
||||||
|
return submitPay(ws, proposalId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
|
||||||
|
|
||||||
let wireFeeLimit;
|
let wireFeeLimit;
|
||||||
if (!proposal.contractTerms.max_wire_fee) {
|
if (!d.contractTerms.max_wire_fee) {
|
||||||
wireFeeLimit = Amounts.getZero(contractAmount.currency);
|
wireFeeLimit = Amounts.getZero(contractAmount.currency);
|
||||||
} else {
|
} else {
|
||||||
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getCoinsForPayment(ws, {
|
const res = await getCoinsForPayment(ws, {
|
||||||
allowedAuditors: proposal.contractTerms.auditors,
|
allowedAuditors: d.contractTerms.auditors,
|
||||||
allowedExchanges: proposal.contractTerms.exchanges,
|
allowedExchanges: d.contractTerms.exchanges,
|
||||||
depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
|
depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
|
||||||
paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
|
paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
|
||||||
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
|
wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
|
||||||
wireFeeLimit,
|
wireFeeLimit,
|
||||||
// FIXME: parse this properly
|
wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
|
||||||
wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
|
wireMethod: d.contractTerms.wire_method,
|
||||||
t_ms: 0,
|
|
||||||
},
|
|
||||||
wireMethod: proposal.contractTerms.wire_method,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace("coin selection result", res);
|
logger.trace("coin selection result", res);
|
||||||
@ -809,7 +840,7 @@ export async function confirmPay(
|
|||||||
if (!sd) {
|
if (!sd) {
|
||||||
const { exchangeUrl, cds, totalAmount } = res;
|
const { exchangeUrl, cds, totalAmount } = res;
|
||||||
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
||||||
proposal.contractTerms,
|
d.contractTerms,
|
||||||
cds,
|
cds,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
);
|
);
|
||||||
@ -823,5 +854,5 @@ export async function confirmPay(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return submitPay(ws, purchase.contractTermsHash, sessionId);
|
return submitPay(ws, proposalId, sessionId);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
import { oneShotIter } from "../util/query";
|
import { runWithReadTransaction } from "../util/query";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
Stores,
|
Stores,
|
||||||
@ -37,187 +37,212 @@ export async function getPendingOperations(
|
|||||||
): Promise<PendingOperationsResponse> {
|
): Promise<PendingOperationsResponse> {
|
||||||
const pendingOperations: PendingOperationInfo[] = [];
|
const pendingOperations: PendingOperationInfo[] = [];
|
||||||
let minRetryDurationMs = 5000;
|
let minRetryDurationMs = 5000;
|
||||||
const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
|
await runWithReadTransaction(
|
||||||
for (let e of exchanges) {
|
ws.db,
|
||||||
switch (e.updateStatus) {
|
[
|
||||||
case ExchangeUpdateStatus.FINISHED:
|
Stores.exchanges,
|
||||||
if (e.lastError) {
|
Stores.reserves,
|
||||||
pendingOperations.push({
|
Stores.refresh,
|
||||||
type: "bug",
|
Stores.coins,
|
||||||
message:
|
Stores.withdrawalSession,
|
||||||
"Exchange record is in FINISHED state but has lastError set",
|
Stores.proposals,
|
||||||
details: {
|
Stores.tips,
|
||||||
|
],
|
||||||
|
async tx => {
|
||||||
|
await tx.iter(Stores.exchanges).forEach(e => {
|
||||||
|
switch (e.updateStatus) {
|
||||||
|
case ExchangeUpdateStatus.FINISHED:
|
||||||
|
if (e.lastError) {
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
message:
|
||||||
|
"Exchange record is in FINISHED state but has lastError set",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!e.details) {
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
message:
|
||||||
|
"Exchange record does not have details, but no update in progress.",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!e.wireInfo) {
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
message:
|
||||||
|
"Exchange record does not have wire info, but no update in progress.",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ExchangeUpdateStatus.FETCH_KEYS:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "exchange-update",
|
||||||
|
stage: "fetch-keys",
|
||||||
exchangeBaseUrl: e.baseUrl,
|
exchangeBaseUrl: e.baseUrl,
|
||||||
},
|
lastError: e.lastError,
|
||||||
|
reason: e.updateReason || "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ExchangeUpdateStatus.FETCH_WIRE:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "exchange-update",
|
||||||
|
stage: "fetch-wire",
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
lastError: e.lastError,
|
||||||
|
reason: e.updateReason || "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
message: "Unknown exchangeUpdateStatus",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
exchangeUpdateStatus: e.updateStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await tx.iter(Stores.reserves).forEach(reserve => {
|
||||||
|
const reserveType = reserve.bankWithdrawStatusUrl
|
||||||
|
? "taler-bank"
|
||||||
|
: "manual";
|
||||||
|
const now = getTimestampNow();
|
||||||
|
switch (reserve.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.DORMANT:
|
||||||
|
// nothing to report as pending
|
||||||
|
break;
|
||||||
|
case ReserveRecordStatus.WITHDRAWING:
|
||||||
|
case ReserveRecordStatus.UNCONFIRMED:
|
||||||
|
case ReserveRecordStatus.QUERYING_STATUS:
|
||||||
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "reserve",
|
||||||
|
stage: reserve.reserveStatus,
|
||||||
|
timestampCreated: reserve.created,
|
||||||
|
reserveType,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
});
|
||||||
|
if (reserve.created.t_ms < now.t_ms - 5000) {
|
||||||
|
minRetryDurationMs = 500;
|
||||||
|
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
||||||
|
minRetryDurationMs = 2000;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "reserve",
|
||||||
|
stage: reserve.reserveStatus,
|
||||||
|
timestampCreated: reserve.created,
|
||||||
|
reserveType,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
|
||||||
|
});
|
||||||
|
if (reserve.created.t_ms < now.t_ms - 5000) {
|
||||||
|
minRetryDurationMs = 500;
|
||||||
|
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
||||||
|
minRetryDurationMs = 2000;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
message: "Unknown reserve record status",
|
||||||
|
details: {
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
reserveStatus: reserve.reserveStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.refresh).forEach(r => {
|
||||||
|
if (r.finished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let refreshStatus: string;
|
||||||
|
if (r.norevealIndex === undefined) {
|
||||||
|
refreshStatus = "melt";
|
||||||
|
} else {
|
||||||
|
refreshStatus = "reveal";
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "refresh",
|
||||||
|
oldCoinPub: r.meltCoinPub,
|
||||||
|
refreshStatus,
|
||||||
|
refreshOutputSize: r.newDenoms.length,
|
||||||
|
refreshSessionId: r.refreshSessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.coins).forEach(coin => {
|
||||||
|
if (coin.status == CoinStatus.Dirty) {
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "dirty-coin",
|
||||||
|
coinPub: coin.coinPub,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!e.details) {
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.withdrawalSession).forEach(ws => {
|
||||||
|
const numCoinsWithdrawn = ws.withdrawn.reduce(
|
||||||
|
(a, x) => a + (x ? 1 : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const numCoinsTotal = ws.withdrawn.length;
|
||||||
|
if (numCoinsWithdrawn < numCoinsTotal) {
|
||||||
pendingOperations.push({
|
pendingOperations.push({
|
||||||
type: "bug",
|
type: "withdraw",
|
||||||
message:
|
numCoinsTotal,
|
||||||
"Exchange record does not have details, but no update in progress.",
|
numCoinsWithdrawn,
|
||||||
details: {
|
source: ws.source,
|
||||||
exchangeBaseUrl: e.baseUrl,
|
withdrawSessionId: ws.withdrawSessionId,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!e.wireInfo) {
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.proposals).forEach((proposal) => {
|
||||||
|
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
||||||
pendingOperations.push({
|
pendingOperations.push({
|
||||||
type: "bug",
|
type: "proposal-choice",
|
||||||
message:
|
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
||||||
"Exchange record does not have wire info, but no update in progress.",
|
proposalId: proposal.proposalId,
|
||||||
details: {
|
proposalTimestamp: proposal.timestamp,
|
||||||
exchangeBaseUrl: e.baseUrl,
|
});
|
||||||
},
|
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
|
||||||
|
pendingOperations.push({
|
||||||
|
type: "proposal-download",
|
||||||
|
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
proposalTimestamp: proposal.timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
});
|
||||||
case ExchangeUpdateStatus.FETCH_KEYS:
|
|
||||||
pendingOperations.push({
|
await tx.iter(Stores.tips).forEach((tip) => {
|
||||||
type: "exchange-update",
|
if (tip.accepted && !tip.pickedUp) {
|
||||||
stage: "fetch-keys",
|
pendingOperations.push({
|
||||||
exchangeBaseUrl: e.baseUrl,
|
type: "tip",
|
||||||
lastError: e.lastError,
|
merchantBaseUrl: tip.merchantBaseUrl,
|
||||||
reason: e.updateReason || "unknown",
|
tipId: tip.tipId,
|
||||||
});
|
merchantTipId: tip.merchantTipId,
|
||||||
break;
|
});
|
||||||
case ExchangeUpdateStatus.FETCH_WIRE:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "exchange-update",
|
|
||||||
stage: "fetch-wire",
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message: "Unknown exchangeUpdateStatus",
|
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
exchangeUpdateStatus: e.updateStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await oneShotIter(ws.db, Stores.reserves).forEach(reserve => {
|
|
||||||
const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : "manual";
|
|
||||||
const now = getTimestampNow();
|
|
||||||
switch (reserve.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.DORMANT:
|
|
||||||
// nothing to report as pending
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WITHDRAWING:
|
|
||||||
case ReserveRecordStatus.UNCONFIRMED:
|
|
||||||
case ReserveRecordStatus.QUERYING_STATUS:
|
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "reserve",
|
|
||||||
stage: reserve.reserveStatus,
|
|
||||||
timestampCreated: reserve.created,
|
|
||||||
reserveType,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
});
|
|
||||||
if (reserve.created.t_ms < now.t_ms - 5000) {
|
|
||||||
minRetryDurationMs = 500;
|
|
||||||
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
|
||||||
minRetryDurationMs = 2000;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "reserve",
|
|
||||||
stage: reserve.reserveStatus,
|
|
||||||
timestampCreated: reserve.created,
|
|
||||||
reserveType,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
|
|
||||||
});
|
|
||||||
if (reserve.created.t_ms < now.t_ms - 5000) {
|
|
||||||
minRetryDurationMs = 500;
|
|
||||||
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
|
||||||
minRetryDurationMs = 2000;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message: "Unknown reserve record status",
|
|
||||||
details: {
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
reserveStatus: reserve.reserveStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await oneShotIter(ws.db, Stores.refresh).forEach(r => {
|
|
||||||
if (r.finished) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let refreshStatus: string;
|
|
||||||
if (r.norevealIndex === undefined) {
|
|
||||||
refreshStatus = "melt";
|
|
||||||
} else {
|
|
||||||
refreshStatus = "reveal";
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "refresh",
|
|
||||||
oldCoinPub: r.meltCoinPub,
|
|
||||||
refreshStatus,
|
|
||||||
refreshOutputSize: r.newDenoms.length,
|
|
||||||
refreshSessionId: r.refreshSessionId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await oneShotIter(ws.db, Stores.coins).forEach(coin => {
|
|
||||||
if (coin.status == CoinStatus.Dirty) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "dirty-coin",
|
|
||||||
coinPub: coin.coinPub,
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
await oneShotIter(ws.db, Stores.withdrawalSession).forEach(ws => {
|
|
||||||
const numCoinsWithdrawn = ws.withdrawn.reduce((a, x) => a + (x ? 1 : 0), 0);
|
|
||||||
const numCoinsTotal = ws.withdrawn.length;
|
|
||||||
if (numCoinsWithdrawn < numCoinsTotal) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "withdraw",
|
|
||||||
numCoinsTotal,
|
|
||||||
numCoinsWithdrawn,
|
|
||||||
source: ws.source,
|
|
||||||
withdrawSessionId: ws.withdrawSessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await oneShotIter(ws.db, Stores.proposals).forEach(proposal => {
|
|
||||||
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "proposal",
|
|
||||||
merchantBaseUrl: proposal.contractTerms.merchant_base_url,
|
|
||||||
proposalId: proposal.proposalId,
|
|
||||||
proposalTimestamp: proposal.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await oneShotIter(ws.db, Stores.tips).forEach(tip => {
|
|
||||||
if (tip.accepted && !tip.pickedUp) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "tip",
|
|
||||||
merchantBaseUrl: tip.merchantBaseUrl,
|
|
||||||
tipId: tip.tipId,
|
|
||||||
merchantTipId: tip.merchantTipId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pendingOperations,
|
pendingOperations,
|
||||||
|
@ -91,13 +91,12 @@ export async function getFullRefundFees(
|
|||||||
|
|
||||||
async function submitRefunds(
|
async function submitRefunds(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
contractTermsHash: string,
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
console.error(
|
console.error(
|
||||||
"not submitting refunds, contract terms not found:",
|
"not submitting refunds, payment not found:",
|
||||||
contractTermsHash,
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -160,7 +159,7 @@ async function submitRefunds(
|
|||||||
ws.db,
|
ws.db,
|
||||||
[Stores.purchases, Stores.coins],
|
[Stores.purchases, Stores.coins],
|
||||||
async tx => {
|
async tx => {
|
||||||
await tx.mutate(Stores.purchases, contractTermsHash, transformPurchase);
|
await tx.mutate(Stores.purchases, proposalId, transformPurchase);
|
||||||
await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
|
await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -344,10 +344,16 @@ async function updateReserve(
|
|||||||
resp = await ws.http.get(reqUrl.href);
|
resp = await ws.http.get(reqUrl.href);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response?.status === 404) {
|
if (e.response?.status === 404) {
|
||||||
return;
|
const m = "The exchange does not know about this reserve (yet).";
|
||||||
|
await setReserveError(ws, reservePub, {
|
||||||
|
type: "waiting",
|
||||||
|
details: {},
|
||||||
|
message: "The exchange does not know about this reserve (yet).",
|
||||||
|
});
|
||||||
|
throw new OperationFailedAndReportedError(m);
|
||||||
} else {
|
} else {
|
||||||
const m = e.message;
|
const m = e.message;
|
||||||
setReserveError(ws, reservePub, {
|
await setReserveError(ws, reservePub, {
|
||||||
type: "network",
|
type: "network",
|
||||||
details: {},
|
details: {},
|
||||||
message: m,
|
message: m,
|
||||||
|
@ -46,6 +46,7 @@ import {
|
|||||||
abortFailedPayment,
|
abortFailedPayment,
|
||||||
preparePay,
|
preparePay,
|
||||||
confirmPay,
|
confirmPay,
|
||||||
|
processDownloadProposal,
|
||||||
} from "./wallet-impl/pay";
|
} from "./wallet-impl/pay";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -227,12 +228,17 @@ export class Wallet {
|
|||||||
case "withdraw":
|
case "withdraw":
|
||||||
await processWithdrawSession(this.ws, pending.withdrawSessionId);
|
await processWithdrawSession(this.ws, pending.withdrawSessionId);
|
||||||
break;
|
break;
|
||||||
case "proposal":
|
case "proposal-choice":
|
||||||
// Nothing to do, user needs to accept/reject
|
// Nothing to do, user needs to accept/reject
|
||||||
break;
|
break;
|
||||||
|
case "proposal-download":
|
||||||
|
await processDownloadProposal(this.ws, pending.proposalId);
|
||||||
|
break;
|
||||||
case "tip":
|
case "tip":
|
||||||
await processTip(this.ws, pending.tipId);
|
await processTip(this.ws, pending.tipId);
|
||||||
break;
|
break;
|
||||||
|
case "pay":
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(pending);
|
assertUnreachable(pending);
|
||||||
}
|
}
|
||||||
|
@ -578,13 +578,25 @@ export interface PendingRefreshOperation {
|
|||||||
refreshOutputSize: number;
|
refreshOutputSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PendingDirtyCoinOperation {
|
export interface PendingDirtyCoinOperation {
|
||||||
type: "dirty-coin";
|
type: "dirty-coin";
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingProposalOperation {
|
export interface PendingProposalDownloadOperation {
|
||||||
type: "proposal";
|
type: "proposal-download";
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
proposalTimestamp: Timestamp;
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User must choose whether to accept or reject the merchant's
|
||||||
|
* proposed contract terms.
|
||||||
|
*/
|
||||||
|
export interface PendingProposalChoiceOperation {
|
||||||
|
type: "proposal-choice";
|
||||||
merchantBaseUrl: string;
|
merchantBaseUrl: string;
|
||||||
proposalTimestamp: Timestamp;
|
proposalTimestamp: Timestamp;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
@ -597,6 +609,12 @@ export interface PendingTipOperation {
|
|||||||
merchantTipId: string;
|
merchantTipId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PendingPayOperation {
|
||||||
|
type: "pay";
|
||||||
|
proposalId: string;
|
||||||
|
isReplay: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type PendingOperationInfo =
|
export type PendingOperationInfo =
|
||||||
| PendingWithdrawOperation
|
| PendingWithdrawOperation
|
||||||
| PendingReserveOperation
|
| PendingReserveOperation
|
||||||
@ -605,7 +623,9 @@ export type PendingOperationInfo =
|
|||||||
| PendingExchangeUpdateOperation
|
| PendingExchangeUpdateOperation
|
||||||
| PendingRefreshOperation
|
| PendingRefreshOperation
|
||||||
| PendingTipOperation
|
| PendingTipOperation
|
||||||
| PendingProposalOperation;
|
| PendingProposalDownloadOperation
|
||||||
|
| PendingProposalChoiceOperation
|
||||||
|
| PendingPayOperation;
|
||||||
|
|
||||||
export interface PendingOperationsResponse {
|
export interface PendingOperationsResponse {
|
||||||
pendingOperations: PendingOperationInfo[];
|
pendingOperations: PendingOperationInfo[];
|
||||||
|
Loading…
Reference in New Issue
Block a user