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;
|
||||
wallet.stop();
|
||||
wp = openPromise<Wallet>();
|
||||
if (walletArgs && walletArgs.persistentStoragePath) {
|
||||
const oldArgs = walletArgs;
|
||||
walletArgs = { ...oldArgs };
|
||||
if (oldArgs && oldArgs.persistentStoragePath) {
|
||||
try {
|
||||
fs.unlinkSync(walletArgs.persistentStoragePath);
|
||||
fs.unlinkSync(oldArgs.persistentStoragePath);
|
||||
} catch (e) {
|
||||
console.error("Error while deleting the wallet db:", e);
|
||||
}
|
||||
@ -210,6 +212,12 @@ export function installAndroidWalletListener() {
|
||||
walletArgs.persistentStoragePath = 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;
|
||||
}
|
||||
default:
|
||||
|
@ -586,9 +586,47 @@ export interface CoinRecord {
|
||||
}
|
||||
|
||||
export enum ProposalStatus {
|
||||
/**
|
||||
* Not downloaded yet.
|
||||
*/
|
||||
DOWNLOADING = "downloading",
|
||||
/**
|
||||
* Proposal downloaded, but the user needs to accept/reject it.
|
||||
*/
|
||||
PROPOSED = "proposed",
|
||||
/**
|
||||
* The user has accepted the proposal.
|
||||
*/
|
||||
ACCEPTED = "accepted",
|
||||
/**
|
||||
* The user has rejected the proposal.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* The contract that was offered by the merchant.
|
||||
* Downloaded data from the merchant.
|
||||
*/
|
||||
@Checkable.Value(() => ContractTerms)
|
||||
contractTerms: ContractTerms;
|
||||
|
||||
/**
|
||||
* Signature by the merchant over the contract details.
|
||||
*/
|
||||
@Checkable.String()
|
||||
merchantSig: string;
|
||||
|
||||
/**
|
||||
* Hash of the contract terms.
|
||||
*/
|
||||
@Checkable.String()
|
||||
contractTermsHash: string;
|
||||
download: ProposalDownload | undefined;
|
||||
|
||||
/**
|
||||
* Unique ID when the order is stored in the wallet DB.
|
||||
@ -639,9 +664,18 @@ export class ProposalRecord {
|
||||
@Checkable.String()
|
||||
noncePriv: string;
|
||||
|
||||
/**
|
||||
* Public key for the nonce.
|
||||
*/
|
||||
@Checkable.String()
|
||||
noncePub: string;
|
||||
|
||||
@Checkable.String()
|
||||
proposalStatus: ProposalStatus;
|
||||
|
||||
@Checkable.String()
|
||||
repurchaseProposalId: string | undefined;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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> {
|
||||
constructor() {
|
||||
super("purchases", { keyPath: "contractTermsHash" });
|
||||
super("purchases", { keyPath: "proposalId" });
|
||||
}
|
||||
|
||||
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
||||
|
@ -25,7 +25,6 @@
|
||||
*/
|
||||
import { openPromise } from "./promiseUtils";
|
||||
|
||||
|
||||
/**
|
||||
* Result of an inner join.
|
||||
*/
|
||||
@ -67,7 +66,7 @@ export interface IndexOptions {
|
||||
}
|
||||
|
||||
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) => {
|
||||
req.onsuccess = () => {
|
||||
resolve(req.result);
|
||||
@ -103,7 +102,7 @@ export async function oneShotGet<T>(
|
||||
): Promise<T | undefined> {
|
||||
const tx = db.transaction([store.name], "readonly");
|
||||
const req = tx.objectStore(store.name).get(key);
|
||||
const v = await requestToPromise(req)
|
||||
const v = await requestToPromise(req);
|
||||
await transactionToPromise(tx);
|
||||
return v;
|
||||
}
|
||||
@ -335,6 +334,17 @@ class TransactionHandle {
|
||||
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> {
|
||||
const req = this.tx.objectStore(store.name).openCursor(key);
|
||||
return new ResultStream<T>(req);
|
||||
@ -407,18 +417,20 @@ function runWithTransaction<T>(
|
||||
};
|
||||
const th = new TransactionHandle(tx);
|
||||
const resP = f(th);
|
||||
resP.then(result => {
|
||||
gotFunResult = true;
|
||||
funResult = result;
|
||||
}).catch((e) => {
|
||||
if (e == TransactionAbort) {
|
||||
console.info("aborting transaction");
|
||||
} else {
|
||||
tx.abort();
|
||||
console.error("Transaction failed:", e);
|
||||
console.error(stack);
|
||||
}
|
||||
});
|
||||
resP
|
||||
.then(result => {
|
||||
gotFunResult = true;
|
||||
funResult = result;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e == TransactionAbort) {
|
||||
console.info("aborting transaction");
|
||||
} else {
|
||||
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
|
||||
// increasing even
|
||||
|
||||
/*
|
||||
const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
|
||||
for (const p of proposals) {
|
||||
history.push({
|
||||
@ -51,6 +52,7 @@ export async function getHistory(
|
||||
explicit: false,
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
const withdrawals = await oneShotIter(
|
||||
ws.db,
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
strcmp,
|
||||
extractTalerStamp,
|
||||
canonicalJson,
|
||||
extractTalerStampOrThrow,
|
||||
} from "../util/helpers";
|
||||
import { Logger } from "../util/logging";
|
||||
import { InternalWalletState } from "./state";
|
||||
@ -320,31 +321,41 @@ async function recordConfirmPay(
|
||||
payCoinInfo: PayCoinInfo,
|
||||
chosenExchange: string,
|
||||
): Promise<PurchaseRecord> {
|
||||
const d = proposal.download;
|
||||
if (!d) {
|
||||
throw Error("proposal is in invalid state");
|
||||
}
|
||||
const payReq: PayReq = {
|
||||
coins: payCoinInfo.sigs,
|
||||
merchant_pub: proposal.contractTerms.merchant_pub,
|
||||
merchant_pub: d.contractTerms.merchant_pub,
|
||||
mode: "pay",
|
||||
order_id: proposal.contractTerms.order_id,
|
||||
order_id: d.contractTerms.order_id,
|
||||
};
|
||||
const t: PurchaseRecord = {
|
||||
abortDone: false,
|
||||
abortRequested: false,
|
||||
contractTerms: proposal.contractTerms,
|
||||
contractTermsHash: proposal.contractTermsHash,
|
||||
contractTerms: d.contractTerms,
|
||||
contractTermsHash: d.contractTermsHash,
|
||||
finished: false,
|
||||
lastSessionId: undefined,
|
||||
merchantSig: proposal.merchantSig,
|
||||
merchantSig: d.merchantSig,
|
||||
payReq,
|
||||
refundsDone: {},
|
||||
refundsPending: {},
|
||||
timestamp: getTimestampNow(),
|
||||
timestamp_refund: undefined,
|
||||
proposalId: proposal.proposalId,
|
||||
};
|
||||
|
||||
await runWithWriteTransaction(
|
||||
ws.db,
|
||||
[Stores.coins, Stores.purchases],
|
||||
[Stores.coins, Stores.purchases, Stores.proposals],
|
||||
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);
|
||||
for (let c of payCoinInfo.updatedCoins) {
|
||||
await tx.put(Stores.coins, c);
|
||||
@ -360,7 +371,7 @@ async function recordConfirmPay(
|
||||
function getNextUrl(contractTerms: ContractTerms): string {
|
||||
const f = contractTerms.fulfillment_url;
|
||||
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);
|
||||
return fu.href;
|
||||
} else {
|
||||
@ -370,9 +381,9 @@ function getNextUrl(contractTerms: ContractTerms): string {
|
||||
|
||||
export async function abortFailedPayment(
|
||||
ws: InternalWalletState,
|
||||
contractTermsHash: string,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||
if (!purchase) {
|
||||
throw Error("Purchase not found, unable to abort with refund");
|
||||
}
|
||||
@ -409,7 +420,7 @@ export async function abortFailedPayment(
|
||||
await acceptRefundResponse(ws, refundResponse);
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -418,30 +429,19 @@ export async function abortFailedPayment(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
export async function processDownloadProposal(
|
||||
ws: InternalWalletState,
|
||||
url: string,
|
||||
sessionId?: string,
|
||||
): Promise<string> {
|
||||
const oldProposal = await oneShotGetIndexed(
|
||||
ws.db,
|
||||
Stores.proposals.urlIndex,
|
||||
url,
|
||||
);
|
||||
if (oldProposal) {
|
||||
return oldProposal.proposalId;
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||
if (!proposal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
|
||||
const parsed_url = new URL(url);
|
||||
parsed_url.searchParams.set("nonce", pub);
|
||||
if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
|
||||
return;
|
||||
}
|
||||
const parsed_url = new URL(proposal.url);
|
||||
parsed_url.searchParams.set("nonce", proposal.noncePub);
|
||||
const urlWithNonce = parsed_url.href;
|
||||
console.log("downloading contract from '" + urlWithNonce + "'");
|
||||
let resp;
|
||||
@ -452,39 +452,103 @@ async function downloadProposal(
|
||||
throw e;
|
||||
}
|
||||
|
||||
const proposal = Proposal.checked(resp.responseJson);
|
||||
const proposalResp = Proposal.checked(resp.responseJson);
|
||||
|
||||
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 proposalRecord: ProposalRecord = {
|
||||
contractTerms: proposal.contract_terms,
|
||||
contractTermsHash,
|
||||
merchantSig: proposal.sig,
|
||||
download: undefined,
|
||||
noncePriv: priv,
|
||||
noncePub: pub,
|
||||
timestamp: getTimestampNow(),
|
||||
url,
|
||||
downloadSessionId: sessionId,
|
||||
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;
|
||||
}
|
||||
|
||||
async function submitPay(
|
||||
export async function submitPay(
|
||||
ws: InternalWalletState,
|
||||
contractTermsHash: string,
|
||||
proposalId: string,
|
||||
sessionId: string | undefined,
|
||||
): Promise<ConfirmPayResult> {
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||
if (!purchase) {
|
||||
throw Error("Purchase not found: " + contractTermsHash);
|
||||
throw Error("Purchase not found: " + proposalId);
|
||||
}
|
||||
if (purchase.abortRequested) {
|
||||
throw Error("not submitting payment for aborted purchase");
|
||||
@ -507,7 +571,7 @@ async function submitPay(
|
||||
const merchantPub = purchase.contractTerms.merchant_pub;
|
||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||
merchantResp.sig,
|
||||
contractTermsHash,
|
||||
purchase.contractTermsHash,
|
||||
merchantPub,
|
||||
);
|
||||
if (!valid) {
|
||||
@ -532,14 +596,16 @@ async function submitPay(
|
||||
[Stores.coins, Stores.purchases],
|
||||
async tx => {
|
||||
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) {
|
||||
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);
|
||||
@ -570,100 +636,67 @@ export async function preparePay(
|
||||
};
|
||||
}
|
||||
|
||||
let proposalId: string;
|
||||
try {
|
||||
proposalId = await downloadProposal(
|
||||
ws,
|
||||
uriResult.downloadUrl,
|
||||
uriResult.sessionId,
|
||||
);
|
||||
} catch (e) {
|
||||
return {
|
||||
status: "error",
|
||||
error: e.toString(),
|
||||
};
|
||||
}
|
||||
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||
const proposalId = await startDownloadProposal(
|
||||
ws,
|
||||
uriResult.downloadUrl,
|
||||
uriResult.sessionId,
|
||||
);
|
||||
|
||||
let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||
if (!proposal) {
|
||||
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);
|
||||
|
||||
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.
|
||||
const purchase = await oneShotGet(
|
||||
ws.db,
|
||||
Stores.purchases,
|
||||
proposal.contractTermsHash,
|
||||
);
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||
|
||||
if (!purchase) {
|
||||
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
|
||||
const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
|
||||
let wireFeeLimit;
|
||||
if (proposal.contractTerms.max_wire_fee) {
|
||||
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
||||
if (contractTerms.max_wire_fee) {
|
||||
wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
|
||||
} else {
|
||||
wireFeeLimit = Amounts.getZero(paymentAmount.currency);
|
||||
}
|
||||
// If not already payed, check if we could pay for it.
|
||||
const res = await getCoinsForPayment(ws, {
|
||||
allowedAuditors: proposal.contractTerms.auditors,
|
||||
allowedExchanges: proposal.contractTerms.exchanges,
|
||||
depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
|
||||
allowedAuditors: contractTerms.auditors,
|
||||
allowedExchanges: contractTerms.exchanges,
|
||||
depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
|
||||
paymentAmount,
|
||||
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
|
||||
wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
|
||||
wireFeeLimit,
|
||||
// FIXME: parse this properly
|
||||
wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
|
||||
t_ms: 0,
|
||||
},
|
||||
wireMethod: proposal.contractTerms.wire_method,
|
||||
wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
|
||||
wireMethod: contractTerms.wire_method,
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
console.log("not confirming payment, insufficient coins");
|
||||
return {
|
||||
status: "insufficient-balance",
|
||||
contractTerms: proposal.contractTerms,
|
||||
contractTerms: contractTerms,
|
||||
proposalId: proposal.proposalId,
|
||||
};
|
||||
}
|
||||
@ -676,7 +709,7 @@ export async function preparePay(
|
||||
) {
|
||||
const { exchangeUrl, cds, totalAmount } = res;
|
||||
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
||||
proposal.contractTerms,
|
||||
contractTerms,
|
||||
cds,
|
||||
totalAmount,
|
||||
);
|
||||
@ -691,19 +724,19 @@ export async function preparePay(
|
||||
|
||||
return {
|
||||
status: "payment-possible",
|
||||
contractTerms: proposal.contractTerms,
|
||||
contractTerms: contractTerms,
|
||||
proposalId: proposal.proposalId,
|
||||
totalFees: res.totalFees,
|
||||
};
|
||||
}
|
||||
|
||||
if (uriResult.sessionId) {
|
||||
await submitPay(ws, purchase.contractTermsHash, uriResult.sessionId);
|
||||
await submitPay(ws, proposalId, uriResult.sessionId);
|
||||
}
|
||||
|
||||
return {
|
||||
status: "paid",
|
||||
contractTerms: proposal.contractTerms,
|
||||
contractTerms: purchase.contractTerms,
|
||||
nextUrl: getNextUrl(purchase.contractTerms),
|
||||
};
|
||||
}
|
||||
@ -762,39 +795,37 @@ export async function confirmPay(
|
||||
throw Error(`proposal with id ${proposalId} not found`);
|
||||
}
|
||||
|
||||
const sessionId = sessionIdOverride || proposal.downloadSessionId;
|
||||
|
||||
let purchase = await oneShotGet(
|
||||
ws.db,
|
||||
Stores.purchases,
|
||||
proposal.contractTermsHash,
|
||||
);
|
||||
|
||||
if (purchase) {
|
||||
return submitPay(ws, purchase.contractTermsHash, sessionId);
|
||||
const d = proposal.download;
|
||||
if (!d) {
|
||||
throw Error("proposal is in invalid state");
|
||||
}
|
||||
|
||||
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;
|
||||
if (!proposal.contractTerms.max_wire_fee) {
|
||||
if (!d.contractTerms.max_wire_fee) {
|
||||
wireFeeLimit = Amounts.getZero(contractAmount.currency);
|
||||
} else {
|
||||
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
||||
wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
|
||||
}
|
||||
|
||||
const res = await getCoinsForPayment(ws, {
|
||||
allowedAuditors: proposal.contractTerms.auditors,
|
||||
allowedExchanges: proposal.contractTerms.exchanges,
|
||||
depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
|
||||
paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
|
||||
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
|
||||
allowedAuditors: d.contractTerms.auditors,
|
||||
allowedExchanges: d.contractTerms.exchanges,
|
||||
depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
|
||||
paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
|
||||
wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
|
||||
wireFeeLimit,
|
||||
// FIXME: parse this properly
|
||||
wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
|
||||
t_ms: 0,
|
||||
},
|
||||
wireMethod: proposal.contractTerms.wire_method,
|
||||
wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
|
||||
wireMethod: d.contractTerms.wire_method,
|
||||
});
|
||||
|
||||
logger.trace("coin selection result", res);
|
||||
@ -809,7 +840,7 @@ export async function confirmPay(
|
||||
if (!sd) {
|
||||
const { exchangeUrl, cds, totalAmount } = res;
|
||||
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
||||
proposal.contractTerms,
|
||||
d.contractTerms,
|
||||
cds,
|
||||
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,
|
||||
getTimestampNow,
|
||||
} from "../walletTypes";
|
||||
import { oneShotIter } from "../util/query";
|
||||
import { runWithReadTransaction } from "../util/query";
|
||||
import { InternalWalletState } from "./state";
|
||||
import {
|
||||
Stores,
|
||||
@ -37,187 +37,212 @@ export async function getPendingOperations(
|
||||
): Promise<PendingOperationsResponse> {
|
||||
const pendingOperations: PendingOperationInfo[] = [];
|
||||
let minRetryDurationMs = 5000;
|
||||
const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
|
||||
for (let e of exchanges) {
|
||||
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: {
|
||||
await runWithReadTransaction(
|
||||
ws.db,
|
||||
[
|
||||
Stores.exchanges,
|
||||
Stores.reserves,
|
||||
Stores.refresh,
|
||||
Stores.coins,
|
||||
Stores.withdrawalSession,
|
||||
Stores.proposals,
|
||||
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,
|
||||
},
|
||||
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({
|
||||
type: "bug",
|
||||
message:
|
||||
"Exchange record does not have details, but no update in progress.",
|
||||
details: {
|
||||
exchangeBaseUrl: e.baseUrl,
|
||||
},
|
||||
type: "withdraw",
|
||||
numCoinsTotal,
|
||||
numCoinsWithdrawn,
|
||||
source: ws.source,
|
||||
withdrawSessionId: ws.withdrawSessionId,
|
||||
});
|
||||
}
|
||||
if (!e.wireInfo) {
|
||||
});
|
||||
|
||||
await tx.iter(Stores.proposals).forEach((proposal) => {
|
||||
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
||||
pendingOperations.push({
|
||||
type: "bug",
|
||||
message:
|
||||
"Exchange record does not have wire info, but no update in progress.",
|
||||
details: {
|
||||
exchangeBaseUrl: e.baseUrl,
|
||||
},
|
||||
type: "proposal-choice",
|
||||
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
||||
proposalId: proposal.proposalId,
|
||||
proposalTimestamp: proposal.timestamp,
|
||||
});
|
||||
} 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({
|
||||
type: "exchange-update",
|
||||
stage: "fetch-keys",
|
||||
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 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;
|
||||
});
|
||||
|
||||
await tx.iter(Stores.tips).forEach((tip) => {
|
||||
if (tip.accepted && !tip.pickedUp) {
|
||||
pendingOperations.push({
|
||||
type: "tip",
|
||||
merchantBaseUrl: tip.merchantBaseUrl,
|
||||
tipId: tip.tipId,
|
||||
merchantTipId: tip.merchantTipId,
|
||||
});
|
||||
}
|
||||
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 {
|
||||
pendingOperations,
|
||||
|
@ -91,13 +91,12 @@ export async function getFullRefundFees(
|
||||
|
||||
async function submitRefunds(
|
||||
ws: InternalWalletState,
|
||||
contractTermsHash: string,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||
if (!purchase) {
|
||||
console.error(
|
||||
"not submitting refunds, contract terms not found:",
|
||||
contractTermsHash,
|
||||
"not submitting refunds, payment not found:",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -160,7 +159,7 @@ async function submitRefunds(
|
||||
ws.db,
|
||||
[Stores.purchases, Stores.coins],
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
@ -344,10 +344,16 @@ async function updateReserve(
|
||||
resp = await ws.http.get(reqUrl.href);
|
||||
} catch (e) {
|
||||
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 {
|
||||
const m = e.message;
|
||||
setReserveError(ws, reservePub, {
|
||||
await setReserveError(ws, reservePub, {
|
||||
type: "network",
|
||||
details: {},
|
||||
message: m,
|
||||
|
@ -46,6 +46,7 @@ import {
|
||||
abortFailedPayment,
|
||||
preparePay,
|
||||
confirmPay,
|
||||
processDownloadProposal,
|
||||
} from "./wallet-impl/pay";
|
||||
|
||||
import {
|
||||
@ -227,12 +228,17 @@ export class Wallet {
|
||||
case "withdraw":
|
||||
await processWithdrawSession(this.ws, pending.withdrawSessionId);
|
||||
break;
|
||||
case "proposal":
|
||||
case "proposal-choice":
|
||||
// Nothing to do, user needs to accept/reject
|
||||
break;
|
||||
case "proposal-download":
|
||||
await processDownloadProposal(this.ws, pending.proposalId);
|
||||
break;
|
||||
case "tip":
|
||||
await processTip(this.ws, pending.tipId);
|
||||
break;
|
||||
case "pay":
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(pending);
|
||||
}
|
||||
|
@ -578,13 +578,25 @@ export interface PendingRefreshOperation {
|
||||
refreshOutputSize: number;
|
||||
}
|
||||
|
||||
|
||||
export interface PendingDirtyCoinOperation {
|
||||
type: "dirty-coin";
|
||||
coinPub: string;
|
||||
}
|
||||
|
||||
export interface PendingProposalOperation {
|
||||
type: "proposal";
|
||||
export interface PendingProposalDownloadOperation {
|
||||
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;
|
||||
proposalTimestamp: Timestamp;
|
||||
proposalId: string;
|
||||
@ -597,6 +609,12 @@ export interface PendingTipOperation {
|
||||
merchantTipId: string;
|
||||
}
|
||||
|
||||
export interface PendingPayOperation {
|
||||
type: "pay";
|
||||
proposalId: string;
|
||||
isReplay: boolean;
|
||||
}
|
||||
|
||||
export type PendingOperationInfo =
|
||||
| PendingWithdrawOperation
|
||||
| PendingReserveOperation
|
||||
@ -605,7 +623,9 @@ export type PendingOperationInfo =
|
||||
| PendingExchangeUpdateOperation
|
||||
| PendingRefreshOperation
|
||||
| PendingTipOperation
|
||||
| PendingProposalOperation;
|
||||
| PendingProposalDownloadOperation
|
||||
| PendingProposalChoiceOperation
|
||||
| PendingPayOperation;
|
||||
|
||||
export interface PendingOperationsResponse {
|
||||
pendingOperations: PendingOperationInfo[];
|
||||
|
Loading…
Reference in New Issue
Block a user