wallet-core: various p2p payment fixes
This commit is contained in:
parent
925ef1f410
commit
e6ed901626
@ -31,9 +31,7 @@ import {
|
||||
export async function runPeerToPeerPullTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
|
||||
t,
|
||||
);
|
||||
const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
|
||||
|
||||
// Withdraw digital cash into the wallet.
|
||||
const wallet1 = new WalletCli(t, "w1");
|
||||
|
@ -2038,7 +2038,7 @@ export interface PreparePeerPushPaymentRequest {
|
||||
|
||||
/**
|
||||
* Instructed amount.
|
||||
*
|
||||
*
|
||||
* FIXME: Allow specifying the instructed amount type.
|
||||
*/
|
||||
amount: AmountString;
|
||||
@ -2092,7 +2092,14 @@ export interface CheckPeerPushPaymentResponse {
|
||||
|
||||
export interface CheckPeerPullPaymentResponse {
|
||||
contractTerms: PeerContractTerms;
|
||||
/**
|
||||
* @deprecated Redundant field with bad name, will be removed soon.
|
||||
*/
|
||||
amount: AmountString;
|
||||
|
||||
amountRaw: AmountString;
|
||||
amountEffective: AmountString;
|
||||
|
||||
peerPullPaymentIncomingId: string;
|
||||
}
|
||||
|
||||
@ -2161,25 +2168,23 @@ export const codecForAcceptPeerPullPaymentRequest =
|
||||
.build("AcceptPeerPllPaymentRequest");
|
||||
|
||||
export interface PreparePeerPullPaymentRequest {
|
||||
exchangeBaseUrl: string;
|
||||
exchangeBaseUrl?: string;
|
||||
amount: AmountString;
|
||||
}
|
||||
export const codecForPreparePeerPullPaymentRequest =
|
||||
(): Codec<PreparePeerPullPaymentRequest> =>
|
||||
buildCodecForObject<PreparePeerPullPaymentRequest>()
|
||||
.property("amount", codecForAmountString())
|
||||
.property("exchangeBaseUrl", codecForString())
|
||||
.property("exchangeBaseUrl", codecOptional(codecForString()))
|
||||
.build("PreparePeerPullPaymentRequest");
|
||||
|
||||
export interface PreparePeerPullPaymentResponse {
|
||||
exchangeBaseUrl: string;
|
||||
amountRaw: AmountString;
|
||||
amountEffective: AmountString;
|
||||
}
|
||||
export interface InitiatePeerPullPaymentRequest {
|
||||
/**
|
||||
* FIXME: Make this optional?
|
||||
*/
|
||||
exchangeBaseUrl: string;
|
||||
exchangeBaseUrl?: string;
|
||||
partialContractTerms: PeerContractTerms;
|
||||
}
|
||||
|
||||
@ -2187,7 +2192,7 @@ export const codecForInitiatePeerPullPaymentRequest =
|
||||
(): Codec<InitiatePeerPullPaymentRequest> =>
|
||||
buildCodecForObject<InitiatePeerPullPaymentRequest>()
|
||||
.property("partialContractTerms", codecForPeerContractTerms())
|
||||
.property("exchangeBaseUrl", codecForString())
|
||||
.property("exchangeBaseUrl", codecOptional(codecForString()))
|
||||
.build("InitiatePeerPullPaymentRequest");
|
||||
|
||||
export interface InitiatePeerPullPaymentResponse {
|
||||
|
@ -18,12 +18,14 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime,
|
||||
addPaytoQueryParams,
|
||||
AgeRestriction,
|
||||
classifyTalerUri,
|
||||
codecForList,
|
||||
codecForString,
|
||||
CoreApiResponse,
|
||||
Duration,
|
||||
encodeCrock,
|
||||
getErrorDetailFromException,
|
||||
getRandomBytes,
|
||||
@ -35,6 +37,7 @@ import {
|
||||
setDangerousTimetravel,
|
||||
setGlobalLogLevelFromString,
|
||||
summarizeTalerErrorDetail,
|
||||
TalerProtocolTimestamp,
|
||||
TalerUriType,
|
||||
WalletNotification,
|
||||
} from "@gnu-taler/taler-util";
|
||||
@ -43,6 +46,7 @@ import {
|
||||
getenv,
|
||||
pathHomedir,
|
||||
processExit,
|
||||
readlinePrompt,
|
||||
setUnhandledRejectionHandler,
|
||||
} from "@gnu-taler/taler-util/compat";
|
||||
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
|
||||
@ -416,7 +420,7 @@ transactionsCli
|
||||
});
|
||||
|
||||
transactionsCli
|
||||
.subcommand("abortTransaction", "delete", {
|
||||
.subcommand("abortTransaction", "abort", {
|
||||
help: "Abort a transaction.",
|
||||
})
|
||||
.requiredArgument("transactionId", clk.STRING, {
|
||||
@ -552,11 +556,16 @@ walletCli
|
||||
.subcommand("handleUri", "handle-uri", {
|
||||
help: "Handle a taler:// URI.",
|
||||
})
|
||||
.requiredArgument("uri", clk.STRING)
|
||||
.maybeArgument("uri", clk.STRING)
|
||||
.flag("autoYes", ["-y", "--yes"])
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const uri: string = args.handleUri.uri;
|
||||
let uri;
|
||||
if (args.handleUri.uri) {
|
||||
uri = args.handleUri.uri;
|
||||
} else {
|
||||
uri = await readlinePrompt("Taler URI: ");
|
||||
}
|
||||
const uriType = classifyTalerUri(uri);
|
||||
switch (uriType) {
|
||||
case TalerUriType.TalerPay:
|
||||
@ -920,6 +929,141 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
||||
help: "Subcommands for advanced operations (only use if you know what you're doing!).",
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("checkPayPull", "check-pay-pull", {
|
||||
help: "Check fees for a peer-pull payment initiation.",
|
||||
})
|
||||
.requiredArgument("amount", clk.STRING, {
|
||||
help: "Amount to request",
|
||||
})
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.PreparePeerPullPayment,
|
||||
{
|
||||
amount: args.checkPayPull.amount,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("prepareIncomingPayPull", "prepare-incoming-pay-pull")
|
||||
.requiredArgument("talerUri", clk.STRING)
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.CheckPeerPullPayment,
|
||||
{
|
||||
talerUri: args.prepareIncomingPayPull.talerUri,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("confirmIncomingPayPull", "confirm-incoming-pay-pull")
|
||||
.requiredArgument("peerPullPaymentIncomingId", clk.STRING)
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.AcceptPeerPullPayment,
|
||||
{
|
||||
peerPullPaymentIncomingId:
|
||||
args.confirmIncomingPayPull.peerPullPaymentIncomingId,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("initiatePayPull", "initiate-pay-pull", {
|
||||
help: "Initiate a peer-pull payment.",
|
||||
})
|
||||
.requiredArgument("amount", clk.STRING, {
|
||||
help: "Amount to request",
|
||||
})
|
||||
.maybeOption("summary", ["--summary"], clk.STRING, {
|
||||
help: "Summary to use in the contract terms.",
|
||||
})
|
||||
.maybeOption("exchangeBaseUrl", ["--exchange"], clk.STRING)
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.InitiatePeerPullPayment,
|
||||
{
|
||||
exchangeBaseUrl: args.initiatePayPull.exchangeBaseUrl,
|
||||
partialContractTerms: {
|
||||
amount: args.initiatePayPull.amount,
|
||||
summary: args.initiatePayPull.summary ?? "Invoice",
|
||||
// FIXME: Make the expiration configurable
|
||||
purse_expiration: AbsoluteTime.toTimestamp(
|
||||
AbsoluteTime.addDuration(
|
||||
AbsoluteTime.now(),
|
||||
Duration.fromSpec({ hours: 1 }),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("checkPayPush", "check-pay-push", {
|
||||
help: "Check fees for a peer-push payment.",
|
||||
})
|
||||
.requiredArgument("amount", clk.STRING, {
|
||||
help: "Amount to pay",
|
||||
})
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.PreparePeerPushPayment,
|
||||
{
|
||||
amount: args.checkPayPush.amount,
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("payPush", "initiate-pay-push", {
|
||||
help: "Initiate a peer-push payment.",
|
||||
})
|
||||
.requiredArgument("amount", clk.STRING, {
|
||||
help: "Amount to pay",
|
||||
})
|
||||
.maybeOption("summary", ["--summary"], clk.STRING, {
|
||||
help: "Summary to use in the contract terms.",
|
||||
})
|
||||
.action(async (args) => {
|
||||
await withWallet(args, async (wallet) => {
|
||||
const resp = await wallet.client.call(
|
||||
WalletApiOperation.InitiatePeerPushPayment,
|
||||
{
|
||||
partialContractTerms: {
|
||||
amount: args.payPush.amount,
|
||||
summary: args.payPush.summary ?? "Payment",
|
||||
// FIXME: Make the expiration configurable
|
||||
purse_expiration: AbsoluteTime.toTimestamp(
|
||||
AbsoluteTime.addDuration(
|
||||
AbsoluteTime.now(),
|
||||
Duration.fromSpec({ hours: 1 }),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
});
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("serve", "serve", {
|
||||
help: "Serve the wallet API via a unix domain socket.",
|
||||
|
@ -54,9 +54,7 @@ import {
|
||||
WireInfo,
|
||||
HashCodeString,
|
||||
Amounts,
|
||||
AttentionPriority,
|
||||
AttentionInfo,
|
||||
AbsoluteTime,
|
||||
Logger,
|
||||
CoinPublicKeyString,
|
||||
} from "@gnu-taler/taler-util";
|
||||
@ -72,7 +70,6 @@ import {
|
||||
StoreWithIndexes,
|
||||
} from "./util/query.js";
|
||||
import { RetryInfo, RetryTags } from "./util/retries.js";
|
||||
import { Wallet } from "./wallet.js";
|
||||
|
||||
/**
|
||||
* This file contains the database schema of the Taler wallet together
|
||||
@ -121,7 +118,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
|
||||
* backwards-compatible way or object stores and indices
|
||||
* are added.
|
||||
*/
|
||||
export const WALLET_DB_MINOR_VERSION = 3;
|
||||
export const WALLET_DB_MINOR_VERSION = 4;
|
||||
|
||||
/**
|
||||
* Ranges for operation status fields.
|
||||
@ -538,6 +535,13 @@ export interface ExchangeRecord {
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
/**
|
||||
* When did we confirm the last withdrawal from this exchange?
|
||||
*
|
||||
* Used mostly in the UI to suggest exchanges.
|
||||
*/
|
||||
lastWithdrawal?: TalerProtocolTimestamp;
|
||||
|
||||
/**
|
||||
* Pointer to the current exchange details.
|
||||
*
|
||||
@ -1852,6 +1856,20 @@ export enum PeerPullPaymentIncomingStatus {
|
||||
Paid = 50 /* DORMANT_START */,
|
||||
}
|
||||
|
||||
export interface PeerPullPaymentCoinSelection {
|
||||
contributions: AmountString[];
|
||||
coinPubs: CoinPublicKeyString[];
|
||||
|
||||
/**
|
||||
* Total cost based on the coin selection.
|
||||
* Non undefined after status === "Accepted"
|
||||
*/
|
||||
totalCost: AmountString | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* AKA PeerPullDebit.
|
||||
*/
|
||||
export interface PeerPullPaymentIncomingRecord {
|
||||
peerPullPaymentIncomingId: string;
|
||||
|
||||
@ -1863,6 +1881,9 @@ export interface PeerPullPaymentIncomingRecord {
|
||||
|
||||
timestampCreated: TalerProtocolTimestamp;
|
||||
|
||||
/**
|
||||
* Contract priv that we got from the other party.
|
||||
*/
|
||||
contractPriv: string;
|
||||
|
||||
/**
|
||||
@ -1871,10 +1892,11 @@ export interface PeerPullPaymentIncomingRecord {
|
||||
status: PeerPullPaymentIncomingStatus;
|
||||
|
||||
/**
|
||||
* Total cost based on the coin selection.
|
||||
* Non undefined after status === "Accepted"
|
||||
* Estimated total cost when the record was created.
|
||||
*/
|
||||
totalCost: AmountString | undefined;
|
||||
totalCostEstimated: AmountString;
|
||||
|
||||
coinSel?: PeerPullPaymentCoinSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2251,6 +2273,14 @@ export const WalletStoresV1 = {
|
||||
"exchangeBaseUrl",
|
||||
"pursePub",
|
||||
]),
|
||||
byExchangeAndContractPriv: describeIndex(
|
||||
"byExchangeAndContractPriv",
|
||||
["exchangeBaseUrl", "contractPriv"],
|
||||
{
|
||||
versionAdded: 4,
|
||||
unique: true,
|
||||
},
|
||||
),
|
||||
byStatus: describeIndex("byStatus", "status"),
|
||||
},
|
||||
),
|
||||
@ -2484,6 +2514,20 @@ export const walletDbFixups: FixupDescription[] = [
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PeerPullPaymentIncomingRecord_totalCostEstimated_add",
|
||||
async fn(tx): Promise<void> {
|
||||
await tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
|
||||
if (pi.totalCostEstimated) {
|
||||
return;
|
||||
}
|
||||
// Not really the cost, but a good substitute for older transactions
|
||||
// that don't sture the effective cost of the transaction.
|
||||
pi.totalCostEstimated = pi.contractTerms.amount;
|
||||
await tx.peerPullPaymentIncoming.put(pi);
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const logger = new Logger("db.ts");
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
OperationAttemptResultType,
|
||||
RetryInfo,
|
||||
} from "../util/retries.js";
|
||||
import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
|
||||
|
||||
const logger = new Logger("operations/common.ts");
|
||||
|
||||
@ -260,6 +261,19 @@ export async function runOperationWithErrorReporting<T1, T2>(
|
||||
return resp;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof CryptoApiStoppedError) {
|
||||
if (ws.stopped) {
|
||||
logger.warn("crypto API stopped during shutdown, ignoring error");
|
||||
return {
|
||||
type: OperationAttemptResultType.Error,
|
||||
errorDetail: makeErrorDetail(
|
||||
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
|
||||
{},
|
||||
"Crypto API stopped during shutdown",
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (e instanceof TalerError) {
|
||||
logger.warn("operation processed resulted in error");
|
||||
logger.warn(`error was: ${j2s(e.errorDetail)}`);
|
||||
|
@ -18,6 +18,7 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime,
|
||||
AcceptPeerPullPaymentRequest,
|
||||
AcceptPeerPullPaymentResponse,
|
||||
AcceptPeerPushPaymentRequest,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
codecForAmountString,
|
||||
codecForAny,
|
||||
codecForExchangeGetContractResponse,
|
||||
codecForPeerContractTerms,
|
||||
CoinStatus,
|
||||
constructPayPullUri,
|
||||
constructPayPushUri,
|
||||
@ -545,6 +547,9 @@ export async function initiatePeerPushPayment(
|
||||
x.peerPushPaymentInitiations,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
// FIXME: Instead of directly doing a spendCoin here,
|
||||
// we might want to mark the coins as used and spend them
|
||||
// after we've been able to create the purse.
|
||||
await spendCoins(ws, tx, {
|
||||
allocationId: `txn:peer-push-debit:${pursePair.pub}`,
|
||||
coinPubs: sel.coins.map((x) => x.coinPub),
|
||||
@ -846,7 +851,77 @@ export async function acceptPeerPushPayment(
|
||||
};
|
||||
}
|
||||
|
||||
export async function acceptPeerPullPayment(
|
||||
export async function processPeerPullDebit(
|
||||
ws: InternalWalletState,
|
||||
peerPullPaymentIncomingId: string,
|
||||
): Promise<OperationAttemptResult> {
|
||||
const peerPullInc = await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||
.runReadOnly(async (tx) => {
|
||||
return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId);
|
||||
});
|
||||
if (!peerPullInc) {
|
||||
throw Error("peer pull debit not found");
|
||||
}
|
||||
if (peerPullInc.status === PeerPullPaymentIncomingStatus.Accepted) {
|
||||
const pursePub = peerPullInc.pursePub;
|
||||
|
||||
const coinSel = peerPullInc.coinSel;
|
||||
if (!coinSel) {
|
||||
throw Error("invalid state, no coins selected");
|
||||
}
|
||||
|
||||
const coins = await queryCoinInfosForSelection(ws, coinSel);
|
||||
|
||||
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
|
||||
exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
|
||||
pursePub: peerPullInc.pursePub,
|
||||
coins,
|
||||
});
|
||||
|
||||
const purseDepositUrl = new URL(
|
||||
`purses/${pursePub}/deposit`,
|
||||
peerPullInc.exchangeBaseUrl,
|
||||
);
|
||||
|
||||
const depositPayload: ExchangePurseDeposits = {
|
||||
deposits: depositSigsResp.deposits,
|
||||
};
|
||||
|
||||
if (logger.shouldLogTrace()) {
|
||||
logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
|
||||
}
|
||||
|
||||
const httpResp = await ws.http.postJson(
|
||||
purseDepositUrl.href,
|
||||
depositPayload,
|
||||
);
|
||||
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||
logger.trace(`purse deposit response: ${j2s(resp)}`);
|
||||
}
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||
.runReadWrite(async (tx) => {
|
||||
const pi = await tx.peerPullPaymentIncoming.get(
|
||||
peerPullPaymentIncomingId,
|
||||
);
|
||||
if (!pi) {
|
||||
throw Error("peer pull payment not found anymore");
|
||||
}
|
||||
if (pi.status === PeerPullPaymentIncomingStatus.Accepted) {
|
||||
pi.status = PeerPullPaymentIncomingStatus.Paid;
|
||||
}
|
||||
await tx.peerPullPaymentIncoming.put(pi);
|
||||
});
|
||||
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
result: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function acceptIncomingPeerPullPayment(
|
||||
ws: InternalWalletState,
|
||||
req: AcceptPeerPullPaymentRequest,
|
||||
): Promise<AcceptPeerPullPaymentResponse> {
|
||||
@ -885,7 +960,7 @@ export async function acceptPeerPullPayment(
|
||||
coinSelRes.result.coins,
|
||||
);
|
||||
|
||||
await ws.db
|
||||
const ppi = await ws.db
|
||||
.mktx((x) => [
|
||||
x.exchanges,
|
||||
x.coins,
|
||||
@ -910,34 +985,26 @@ export async function acceptPeerPullPayment(
|
||||
if (!pi) {
|
||||
throw Error();
|
||||
}
|
||||
pi.status = PeerPullPaymentIncomingStatus.Accepted;
|
||||
pi.totalCost = Amounts.stringify(totalAmount);
|
||||
if (pi.status === PeerPullPaymentIncomingStatus.Proposed) {
|
||||
pi.status = PeerPullPaymentIncomingStatus.Accepted;
|
||||
pi.coinSel = {
|
||||
coinPubs: sel.coins.map((x) => x.coinPub),
|
||||
contributions: sel.coins.map((x) => x.contribution),
|
||||
totalCost: Amounts.stringify(totalAmount),
|
||||
};
|
||||
}
|
||||
await tx.peerPullPaymentIncoming.put(pi);
|
||||
return pi;
|
||||
});
|
||||
|
||||
const pursePub = peerPullInc.pursePub;
|
||||
|
||||
const coinSel = coinSelRes.result;
|
||||
|
||||
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
|
||||
exchangeBaseUrl: coinSel.exchangeBaseUrl,
|
||||
pursePub,
|
||||
coins: coinSel.coins,
|
||||
});
|
||||
|
||||
const purseDepositUrl = new URL(
|
||||
`purses/${pursePub}/deposit`,
|
||||
coinSel.exchangeBaseUrl,
|
||||
await runOperationWithErrorReporting(
|
||||
ws,
|
||||
RetryTags.forPeerPullPaymentDebit(ppi),
|
||||
async () => {
|
||||
return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId);
|
||||
},
|
||||
);
|
||||
|
||||
const depositPayload: ExchangePurseDeposits = {
|
||||
deposits: depositSigsResp.deposits,
|
||||
};
|
||||
|
||||
const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
|
||||
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||
logger.trace(`purse deposit response: ${j2s(resp)}`);
|
||||
|
||||
return {
|
||||
transactionId: makeTransactionId(
|
||||
TransactionType.PeerPullDebit,
|
||||
@ -946,14 +1013,38 @@ export async function acceptPeerPullPayment(
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkPeerPullPayment(
|
||||
/**
|
||||
* Look up information about an incoming peer pull payment.
|
||||
* Store the results in the wallet DB.
|
||||
*/
|
||||
export async function prepareIncomingPeerPullPayment(
|
||||
ws: InternalWalletState,
|
||||
req: CheckPeerPullPaymentRequest,
|
||||
): Promise<CheckPeerPullPaymentResponse> {
|
||||
const uri = parsePayPullUri(req.talerUri);
|
||||
|
||||
if (!uri) {
|
||||
throw Error("got invalid taler://pay-push URI");
|
||||
throw Error("got invalid taler://pay-pull URI");
|
||||
}
|
||||
|
||||
const existingPullIncomingRecord = await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||
.runReadOnly(async (tx) => {
|
||||
return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([
|
||||
uri.exchangeBaseUrl,
|
||||
uri.contractPriv,
|
||||
]);
|
||||
});
|
||||
|
||||
if (existingPullIncomingRecord) {
|
||||
return {
|
||||
amount: existingPullIncomingRecord.contractTerms.amount,
|
||||
amountRaw: existingPullIncomingRecord.contractTerms.amount,
|
||||
amountEffective: existingPullIncomingRecord.totalCostEstimated,
|
||||
contractTerms: existingPullIncomingRecord.contractTerms,
|
||||
peerPullPaymentIncomingId:
|
||||
existingPullIncomingRecord.peerPullPaymentIncomingId,
|
||||
};
|
||||
}
|
||||
|
||||
const exchangeBaseUrl = uri.exchangeBaseUrl;
|
||||
@ -988,6 +1079,38 @@ export async function checkPeerPullPayment(
|
||||
|
||||
const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32));
|
||||
|
||||
let contractTerms: PeerContractTerms;
|
||||
|
||||
if (dec.contractTerms) {
|
||||
contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
|
||||
// FIXME: Check that the purseStatus balance matches contract terms amount
|
||||
} else {
|
||||
// FIXME: In this case, where do we get the purse expiration from?!
|
||||
// https://bugs.gnunet.org/view.php?id=7706
|
||||
throw Error("pull payments without contract terms not supported yet");
|
||||
}
|
||||
|
||||
// FIXME: Why don't we compute the totalCost here?!
|
||||
|
||||
const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
|
||||
|
||||
const coinSelRes = await selectPeerCoins(ws, instructedAmount);
|
||||
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
||||
|
||||
if (coinSelRes.type !== "success") {
|
||||
throw TalerError.fromDetail(
|
||||
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||
{
|
||||
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const totalAmount = await getTotalPeerPaymentCost(
|
||||
ws,
|
||||
coinSelRes.result.coins,
|
||||
);
|
||||
|
||||
await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentIncoming])
|
||||
.runReadWrite(async (tx) => {
|
||||
@ -997,15 +1120,17 @@ export async function checkPeerPullPayment(
|
||||
exchangeBaseUrl: exchangeBaseUrl,
|
||||
pursePub: pursePub,
|
||||
timestampCreated: TalerProtocolTimestamp.now(),
|
||||
contractTerms: dec.contractTerms,
|
||||
contractTerms,
|
||||
status: PeerPullPaymentIncomingStatus.Proposed,
|
||||
totalCost: undefined,
|
||||
totalCostEstimated: Amounts.stringify(totalAmount),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
amount: purseStatus.balance,
|
||||
contractTerms: dec.contractTerms,
|
||||
amount: contractTerms.amount,
|
||||
amountEffective: Amounts.stringify(totalAmount),
|
||||
amountRaw: contractTerms.amount,
|
||||
contractTerms: contractTerms,
|
||||
peerPullPaymentIncomingId,
|
||||
};
|
||||
}
|
||||
@ -1119,12 +1244,75 @@ export async function processPeerPullInitiation(
|
||||
};
|
||||
}
|
||||
|
||||
export async function preparePeerPullPayment(
|
||||
/**
|
||||
* Find a prefered exchange based on when we withdrew last from this exchange.
|
||||
*/
|
||||
async function getPreferredExchangeForCurrency(
|
||||
ws: InternalWalletState,
|
||||
currency: string,
|
||||
): Promise<string | undefined> {
|
||||
// Find an exchange with the matching currency.
|
||||
// Prefer exchanges with the most recent withdrawal.
|
||||
const url = await ws.db
|
||||
.mktx((x) => [x.exchanges])
|
||||
.runReadOnly(async (tx) => {
|
||||
const exchanges = await tx.exchanges.iter().toArray();
|
||||
let candidate = undefined;
|
||||
for (const e of exchanges) {
|
||||
if (e.detailsPointer?.currency !== currency) {
|
||||
continue;
|
||||
}
|
||||
if (!candidate) {
|
||||
candidate = e;
|
||||
continue;
|
||||
}
|
||||
if (candidate.lastWithdrawal && !e.lastWithdrawal) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.lastWithdrawal && e.lastWithdrawal) {
|
||||
if (
|
||||
AbsoluteTime.cmp(
|
||||
AbsoluteTime.fromTimestamp(e.lastWithdrawal),
|
||||
AbsoluteTime.fromTimestamp(candidate.lastWithdrawal),
|
||||
) > 0
|
||||
) {
|
||||
candidate = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidate) {
|
||||
return candidate.baseUrl;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check fees and available exchanges for a peer push payment initiation.
|
||||
*/
|
||||
export async function checkPeerPullPaymentInitiation(
|
||||
ws: InternalWalletState,
|
||||
req: PreparePeerPullPaymentRequest,
|
||||
): Promise<PreparePeerPullPaymentResponse> {
|
||||
//FIXME: look up for exchange details and use purse fee
|
||||
// FIXME: We don't support exchanges with purse fees yet.
|
||||
// Select an exchange where we have money in the specified currency
|
||||
// FIXME: How do we handle regional currency scopes here? Is it an additional input?
|
||||
|
||||
const currency = Amounts.currencyOf(req.amount);
|
||||
let exchangeUrl;
|
||||
if (req.exchangeBaseUrl) {
|
||||
exchangeUrl = req.exchangeBaseUrl;
|
||||
} else {
|
||||
exchangeUrl = await getPreferredExchangeForCurrency(ws, currency);
|
||||
}
|
||||
|
||||
if (!exchangeUrl) {
|
||||
throw Error("no exchange found for initiating a peer pull payment");
|
||||
}
|
||||
|
||||
return {
|
||||
exchangeBaseUrl: exchangeUrl,
|
||||
amountEffective: req.amount,
|
||||
amountRaw: req.amount,
|
||||
};
|
||||
@ -1137,10 +1325,24 @@ export async function initiatePeerPullPayment(
|
||||
ws: InternalWalletState,
|
||||
req: InitiatePeerPullPaymentRequest,
|
||||
): Promise<InitiatePeerPullPaymentResponse> {
|
||||
await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
|
||||
const currency = Amounts.currencyOf(req.partialContractTerms.amount);
|
||||
let maybeExchangeBaseUrl: string | undefined;
|
||||
if (req.exchangeBaseUrl) {
|
||||
maybeExchangeBaseUrl = req.exchangeBaseUrl;
|
||||
} else {
|
||||
maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency);
|
||||
}
|
||||
|
||||
if (!maybeExchangeBaseUrl) {
|
||||
throw Error("no exchange found for initiating a peer pull payment");
|
||||
}
|
||||
|
||||
const exchangeBaseUrl = maybeExchangeBaseUrl;
|
||||
|
||||
await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
||||
|
||||
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||
exchangeBaseUrl: exchangeBaseUrl,
|
||||
});
|
||||
|
||||
const mergeTimestamp = TalerProtocolTimestamp.now();
|
||||
@ -1166,7 +1368,7 @@ export async function initiatePeerPullPayment(
|
||||
await tx.peerPullPaymentInitiations.put({
|
||||
amount: req.partialContractTerms.amount,
|
||||
contractTermsHash: hContractTerms,
|
||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||
exchangeBaseUrl: exchangeBaseUrl,
|
||||
pursePriv: pursePair.priv,
|
||||
pursePub: pursePair.pub,
|
||||
mergePriv: mergePair.priv,
|
||||
@ -1196,6 +1398,9 @@ export async function initiatePeerPullPayment(
|
||||
},
|
||||
);
|
||||
|
||||
// FIXME: Why do we create this only here?
|
||||
// What if the previous operation didn't succeed?
|
||||
|
||||
const wg = await internalCreateWithdrawalGroup(ws, {
|
||||
amount: instructedAmount,
|
||||
wgInfo: {
|
||||
@ -1203,7 +1408,7 @@ export async function initiatePeerPullPayment(
|
||||
contractTerms,
|
||||
contractPriv: contractKeyPair.priv,
|
||||
},
|
||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||
exchangeBaseUrl: exchangeBaseUrl,
|
||||
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
|
||||
reserveKeyPair: {
|
||||
priv: mergeReserveInfo.reservePriv,
|
||||
@ -1213,7 +1418,7 @@ export async function initiatePeerPullPayment(
|
||||
|
||||
return {
|
||||
talerUri: constructPayPullUri({
|
||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||
exchangeBaseUrl: exchangeBaseUrl,
|
||||
contractPriv: contractKeyPair.priv,
|
||||
}),
|
||||
transactionId: makeTransactionId(
|
||||
@ -1222,5 +1427,3 @@ export async function initiatePeerPullPayment(
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
OperationStatus,
|
||||
OperationStatusRange,
|
||||
PeerPushPaymentInitiationStatus,
|
||||
PeerPullPaymentIncomingStatus,
|
||||
} from "../db.js";
|
||||
import {
|
||||
PendingOperationsResponse,
|
||||
@ -377,6 +378,32 @@ async function gatherPeerPullInitiationPending(
|
||||
});
|
||||
}
|
||||
|
||||
async function gatherPeerPullDebitPending(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{
|
||||
peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming;
|
||||
operationRetries: typeof WalletStoresV1.operationRetries;
|
||||
}>,
|
||||
now: AbsoluteTime,
|
||||
resp: PendingOperationsResponse,
|
||||
): Promise<void> {
|
||||
await tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
|
||||
if (pi.status === PeerPullPaymentIncomingStatus.Paid) {
|
||||
return;
|
||||
}
|
||||
const opId = RetryTags.forPeerPullPaymentDebit(pi);
|
||||
const retryRecord = await tx.operationRetries.get(opId);
|
||||
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
|
||||
resp.pendingOperations.push({
|
||||
type: PendingTaskType.PeerPullDebit,
|
||||
...getPendingCommon(ws, opId, timestampDue),
|
||||
givesLifeness: true,
|
||||
retryInfo: retryRecord?.retryInfo,
|
||||
peerPullPaymentIncomingId: pi.peerPullPaymentIncomingId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function gatherPeerPushInitiationPending(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{
|
||||
@ -423,6 +450,7 @@ export async function getPendingOperations(
|
||||
x.operationRetries,
|
||||
x.peerPullPaymentInitiations,
|
||||
x.peerPushPaymentInitiations,
|
||||
x.peerPullPaymentIncoming,
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const resp: PendingOperationsResponse = {
|
||||
@ -438,6 +466,7 @@ export async function getPendingOperations(
|
||||
await gatherBackupPending(ws, tx, now, resp);
|
||||
await gatherPeerPushInitiationPending(ws, tx, now, resp);
|
||||
await gatherPeerPullInitiationPending(ws, tx, now, resp);
|
||||
await gatherPeerPullDebitPending(ws, tx, now, resp);
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
constructPayPullUri,
|
||||
constructPayPushUri,
|
||||
ExtendedStatus,
|
||||
j2s,
|
||||
Logger,
|
||||
OrderShortInfo,
|
||||
PaymentStatus,
|
||||
@ -402,8 +401,8 @@ function buildTransactionForPullPaymentDebit(
|
||||
): Transaction {
|
||||
return {
|
||||
type: TransactionType.PeerPullDebit,
|
||||
amountEffective: pi.totalCost
|
||||
? pi.totalCost
|
||||
amountEffective: pi.coinSel?.totalCost
|
||||
? pi.coinSel?.totalCost
|
||||
: Amounts.stringify(pi.contractTerms.amount),
|
||||
amountRaw: Amounts.stringify(pi.contractTerms.amount),
|
||||
exchangeBaseUrl: pi.exchangeBaseUrl,
|
||||
|
@ -1914,6 +1914,12 @@ export async function internalCreateWithdrawalGroup(
|
||||
reservePriv: withdrawalGroup.reservePriv,
|
||||
});
|
||||
|
||||
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
|
||||
if (exchange) {
|
||||
exchange.lastWithdrawal = TalerProtocolTimestamp.now();
|
||||
await tx.exchanges.put(exchange);
|
||||
}
|
||||
|
||||
if (!isAudited && !isTrusted) {
|
||||
await tx.exchangeTrust.put({
|
||||
currency: amount.currency,
|
||||
|
@ -39,6 +39,7 @@ export enum PendingTaskType {
|
||||
Backup = "backup",
|
||||
PeerPushInitiation = "peer-push-initiation",
|
||||
PeerPullInitiation = "peer-pull-initiation",
|
||||
PeerPullDebit = "peer-pull-debit",
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,6 +58,7 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
|
||||
| PendingBackupTask
|
||||
| PendingPeerPushInitiationTask
|
||||
| PendingPeerPullInitiationTask
|
||||
| PendingPeerPullDebitTask
|
||||
);
|
||||
|
||||
export interface PendingBackupTask {
|
||||
@ -90,6 +92,14 @@ export interface PendingPeerPullInitiationTask {
|
||||
pursePub: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wallet wants to send a peer pull payment.
|
||||
*/
|
||||
export interface PendingPeerPullDebitTask {
|
||||
type: PendingTaskType.PeerPullDebit;
|
||||
peerPullPaymentIncomingId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wallet should check whether coins from this exchange
|
||||
* need to be auto-refreshed.
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
BackupProviderRecord,
|
||||
DepositGroupRecord,
|
||||
ExchangeRecord,
|
||||
PeerPullPaymentIncomingRecord,
|
||||
PeerPullPaymentInitiationRecord,
|
||||
PeerPushPaymentInitiationRecord,
|
||||
PurchaseRecord,
|
||||
@ -215,6 +216,11 @@ export namespace RetryTags {
|
||||
): string {
|
||||
return `${PendingTaskType.PeerPullInitiation}:${ppi.pursePub}`;
|
||||
}
|
||||
export function forPeerPullPaymentDebit(
|
||||
ppi: PeerPullPaymentIncomingRecord,
|
||||
): string {
|
||||
return `${PendingTaskType.PeerPullDebit}:${ppi.pursePub}`;
|
||||
}
|
||||
export function byPaymentProposalId(proposalId: string): string {
|
||||
return `${PendingTaskType.Purchase}:${proposalId}`;
|
||||
}
|
||||
|
@ -613,7 +613,7 @@ export type InitiatePeerPushPaymentOp = {
|
||||
|
||||
/**
|
||||
* Check an incoming peer push payment.
|
||||
*
|
||||
*
|
||||
* FIXME: Rename to "PrepareIncomingPeerPushPayment"
|
||||
*/
|
||||
export type CheckPeerPushPaymentOp = {
|
||||
@ -624,6 +624,8 @@ export type CheckPeerPushPaymentOp = {
|
||||
|
||||
/**
|
||||
* Accept an incoming peer push payment.
|
||||
*
|
||||
* FIXME: Rename to ConfirmIncomingPeerPushPayment
|
||||
*/
|
||||
export type AcceptPeerPushPaymentOp = {
|
||||
op: WalletApiOperation.AcceptPeerPushPayment;
|
||||
@ -633,7 +635,7 @@ export type AcceptPeerPushPaymentOp = {
|
||||
|
||||
/**
|
||||
* Initiate an outgoing peer pull payment.
|
||||
*
|
||||
*
|
||||
* FIXME: This does not check anything, so rename to CheckPeerPullPaymentInitiation
|
||||
*/
|
||||
export type PreparePeerPullPaymentOp = {
|
||||
@ -654,7 +656,7 @@ export type InitiatePeerPullPaymentOp = {
|
||||
/**
|
||||
* Prepare for an incoming peer pull payment.
|
||||
*
|
||||
* FIXME: Rename to "PreparePeerPullPayment"
|
||||
* FIXME: Rename to "PrepareIncomingPeerPullPayment"
|
||||
*/
|
||||
export type CheckPeerPullPaymentOp = {
|
||||
op: WalletApiOperation.CheckPeerPullPayment;
|
||||
@ -665,7 +667,7 @@ export type CheckPeerPullPaymentOp = {
|
||||
/**
|
||||
* Accept an incoming peer pull payment (i.e. pay the other party).
|
||||
*
|
||||
* FIXME: Rename to ConfirmPeerPullPayment
|
||||
* FIXME: Rename to ConfirmIncomingPeerPullPayment
|
||||
*/
|
||||
export type AcceptPeerPullPaymentOp = {
|
||||
op: WalletApiOperation.AcceptPeerPullPayment;
|
||||
|
@ -195,16 +195,17 @@ import {
|
||||
processPurchase,
|
||||
} from "./operations/pay-merchant.js";
|
||||
import {
|
||||
acceptPeerPullPayment,
|
||||
acceptIncomingPeerPullPayment,
|
||||
acceptPeerPushPayment,
|
||||
checkPeerPullPayment,
|
||||
prepareIncomingPeerPullPayment,
|
||||
checkPeerPushPayment,
|
||||
initiatePeerPullPayment,
|
||||
initiatePeerPushPayment,
|
||||
preparePeerPullPayment,
|
||||
checkPeerPullPaymentInitiation,
|
||||
preparePeerPushPayment,
|
||||
processPeerPullInitiation,
|
||||
processPeerPushInitiation,
|
||||
processPeerPullDebit,
|
||||
} from "./operations/pay-peer.js";
|
||||
import { getPendingOperations } from "./operations/pending.js";
|
||||
import {
|
||||
@ -328,6 +329,8 @@ async function callOperationHandler(
|
||||
return await processPeerPushInitiation(ws, pending.pursePub);
|
||||
case PendingTaskType.PeerPullInitiation:
|
||||
return await processPeerPullInitiation(ws, pending.pursePub);
|
||||
case PendingTaskType.PeerPullDebit:
|
||||
return await processPeerPullDebit(ws, pending.peerPullPaymentIncomingId);
|
||||
default:
|
||||
return assertUnreachable(pending);
|
||||
}
|
||||
@ -1440,7 +1443,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
}
|
||||
case WalletApiOperation.PreparePeerPullPayment: {
|
||||
const req = codecForPreparePeerPullPaymentRequest().decode(payload);
|
||||
return await preparePeerPullPayment(ws, req);
|
||||
return await checkPeerPullPaymentInitiation(ws, req);
|
||||
}
|
||||
case WalletApiOperation.InitiatePeerPullPayment: {
|
||||
const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
|
||||
@ -1448,11 +1451,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
}
|
||||
case WalletApiOperation.CheckPeerPullPayment: {
|
||||
const req = codecForCheckPeerPullPaymentRequest().decode(payload);
|
||||
return await checkPeerPullPayment(ws, req);
|
||||
return await prepareIncomingPeerPullPayment(ws, req);
|
||||
}
|
||||
case WalletApiOperation.AcceptPeerPullPayment: {
|
||||
const req = codecForAcceptPeerPullPaymentRequest().decode(payload);
|
||||
return await acceptPeerPullPayment(ws, req);
|
||||
return await acceptIncomingPeerPullPayment(ws, req);
|
||||
}
|
||||
case WalletApiOperation.ApplyDevExperiment: {
|
||||
const req = codecForApplyDevExperiment().decode(payload);
|
||||
|
Loading…
Reference in New Issue
Block a user