feature: 7440 add expiration to p2p

This commit is contained in:
Sebastian 2022-11-08 13:00:34 -03:00
parent 43c7cff750
commit 5c742afbdf
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
19 changed files with 441 additions and 125 deletions

View File

@ -139,12 +139,13 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
let iban: string | undefined = undefined; let iban: string | undefined = undefined;
let bic: string | undefined = undefined; let bic: string | undefined = undefined;
if (parts.length === 1) { if (parts.length === 1) {
iban = parts[0] iban = parts[0];
} if (parts.length === 2) { }
bic = parts[0] if (parts.length === 2) {
iban = parts[1] bic = parts[0];
iban = parts[1];
} else { } else {
iban = targetPath iban = targetPath;
} }
return { return {
isKnown: true, isKnown: true,

View File

@ -1297,7 +1297,7 @@ export const codecForProduct = (): Codec<Product> =>
.property("price", codecOptional(codecForString())) .property("price", codecOptional(codecForString()))
.build("Tax"); .build("Tax");
export const codecForContractTerms = (): Codec<MerchantContractTerms> => export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
buildCodecForObject<MerchantContractTerms>() buildCodecForObject<MerchantContractTerms>()
.property("order_id", codecForString()) .property("order_id", codecForString())
.property("fulfillment_url", codecOptional(codecForString())) .property("fulfillment_url", codecOptional(codecForString()))
@ -1329,7 +1329,14 @@ export const codecForContractTerms = (): Codec<MerchantContractTerms> =>
.property("products", codecOptional(codecForList(codecForProduct()))) .property("products", codecOptional(codecForList(codecForProduct())))
.property("extra", codecForAny()) .property("extra", codecForAny())
.property("minimum_age", codecOptional(codecForNumber())) .property("minimum_age", codecOptional(codecForNumber()))
.build("ContractTerms"); .build("MerchantContractTerms");
export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
buildCodecForObject<PeerContractTerms>()
.property("summary", codecForString())
.property("amount", codecForString())
.property("purse_expiration", codecForTimestamp)
.build("PeerContractTerms");
export const codecForMerchantRefundPermission = export const codecForMerchantRefundPermission =
(): Codec<MerchantAbortPayRefundDetails> => (): Codec<MerchantAbortPayRefundDetails> =>

View File

@ -15,7 +15,7 @@
*/ */
import test from "ava"; import test from "ava";
import { codecForContractTerms } from "./taler-types.js"; import { codecForMerchantContractTerms as codecForContractTerms } from "./taler-types.js";
test("contract terms validation", (t) => { test("contract terms validation", (t) => {
const c = { const c = {

View File

@ -53,13 +53,15 @@ import { TalerErrorCode } from "./taler-error-codes.js";
import { import {
AmountString, AmountString,
AuditorDenomSig, AuditorDenomSig,
codecForContractTerms, codecForMerchantContractTerms,
CoinEnvelope, CoinEnvelope,
MerchantContractTerms, MerchantContractTerms,
PeerContractTerms,
DenominationPubKey, DenominationPubKey,
DenomKeyType, DenomKeyType,
ExchangeAuditor, ExchangeAuditor,
UnblindedSignature, UnblindedSignature,
codecForPeerContractTerms,
} from "./taler-types.js"; } from "./taler-types.js";
import { import {
AbsoluteTime, AbsoluteTime,
@ -253,7 +255,7 @@ export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
buildCodecForObject<ConfirmPayResultDone>() buildCodecForObject<ConfirmPayResultDone>()
.property("type", codecForConstString(ConfirmPayResultType.Done)) .property("type", codecForConstString(ConfirmPayResultType.Done))
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.property("contractTerms", codecForContractTerms()) .property("contractTerms", codecForMerchantContractTerms())
.build("ConfirmPayResultDone"); .build("ConfirmPayResultDone");
export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> => export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
@ -383,7 +385,7 @@ export const codecForPreparePayResultPaymentPossible =
buildCodecForObject<PreparePayResultPaymentPossible>() buildCodecForObject<PreparePayResultPaymentPossible>()
.property("amountEffective", codecForAmountString()) .property("amountEffective", codecForAmountString())
.property("amountRaw", codecForAmountString()) .property("amountRaw", codecForAmountString())
.property("contractTerms", codecForContractTerms()) .property("contractTerms", codecForMerchantContractTerms())
.property("proposalId", codecForString()) .property("proposalId", codecForString())
.property("contractTermsHash", codecForString()) .property("contractTermsHash", codecForString())
.property("noncePriv", codecForString()) .property("noncePriv", codecForString())
@ -1738,9 +1740,26 @@ export interface PayCoinSelection {
customerDepositFees: AmountString; customerDepositFees: AmountString;
} }
export interface InitiatePeerPushPaymentRequest { export interface PreparePeerPushPaymentRequest {
exchangeBaseUrl?: string;
amount: AmountString; amount: AmountString;
partialContractTerms: any; }
export const codecForPreparePeerPushPaymentRequest =
(): Codec<PreparePeerPushPaymentRequest> =>
buildCodecForObject<PreparePeerPushPaymentRequest>()
.property("exchangeBaseUrl", codecOptional(codecForString()))
.property("amount", codecForAmountString())
.build("InitiatePeerPushPaymentRequest");
export interface PreparePeerPushPaymentResponse {
amountRaw: AmountString;
amountEffective: AmountString;
}
export interface InitiatePeerPushPaymentRequest {
exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
} }
export interface InitiatePeerPushPaymentResponse { export interface InitiatePeerPushPaymentResponse {
@ -1755,8 +1774,7 @@ export interface InitiatePeerPushPaymentResponse {
export const codecForInitiatePeerPushPaymentRequest = export const codecForInitiatePeerPushPaymentRequest =
(): Codec<InitiatePeerPushPaymentRequest> => (): Codec<InitiatePeerPushPaymentRequest> =>
buildCodecForObject<InitiatePeerPushPaymentRequest>() buildCodecForObject<InitiatePeerPushPaymentRequest>()
.property("amount", codecForAmountString()) .property("partialContractTerms", codecForPeerContractTerms())
.property("partialContractTerms", codecForAny())
.build("InitiatePeerPushPaymentRequest"); .build("InitiatePeerPushPaymentRequest");
export interface CheckPeerPushPaymentRequest { export interface CheckPeerPushPaymentRequest {
@ -1768,13 +1786,13 @@ export interface CheckPeerPullPaymentRequest {
} }
export interface CheckPeerPushPaymentResponse { export interface CheckPeerPushPaymentResponse {
contractTerms: any; contractTerms: PeerContractTerms;
amount: AmountString; amount: AmountString;
peerPushPaymentIncomingId: string; peerPushPaymentIncomingId: string;
} }
export interface CheckPeerPullPaymentResponse { export interface CheckPeerPullPaymentResponse {
contractTerms: any; contractTerms: PeerContractTerms;
amount: AmountString; amount: AmountString;
peerPullPaymentIncomingId: string; peerPullPaymentIncomingId: string;
} }
@ -1843,21 +1861,34 @@ export const codecForAcceptPeerPullPaymentRequest =
.property("peerPullPaymentIncomingId", codecForString()) .property("peerPullPaymentIncomingId", codecForString())
.build("AcceptPeerPllPaymentRequest"); .build("AcceptPeerPllPaymentRequest");
export interface PreparePeerPullPaymentRequest {
exchangeBaseUrl: string;
amount: AmountString;
}
export const codecForPreparePeerPullPaymentRequest =
(): Codec<PreparePeerPullPaymentRequest> =>
buildCodecForObject<PreparePeerPullPaymentRequest>()
.property("amount", codecForAmountString())
.property("exchangeBaseUrl", codecForString())
.build("PreparePeerPullPaymentRequest");
export interface PreparePeerPullPaymentResponse {
amountRaw: AmountString;
amountEffective: AmountString;
}
export interface InitiatePeerPullPaymentRequest { export interface InitiatePeerPullPaymentRequest {
/** /**
* FIXME: Make this optional? * FIXME: Make this optional?
*/ */
exchangeBaseUrl: string; exchangeBaseUrl: string;
amount: AmountString; partialContractTerms: PeerContractTerms;
partialContractTerms: any;
} }
export const codecForInitiatePeerPullPaymentRequest = export const codecForInitiatePeerPullPaymentRequest =
(): Codec<InitiatePeerPullPaymentRequest> => (): Codec<InitiatePeerPullPaymentRequest> =>
buildCodecForObject<InitiatePeerPullPaymentRequest>() buildCodecForObject<InitiatePeerPullPaymentRequest>()
.property("partialContractTerms", codecForAny()) .property("partialContractTerms", codecForPeerContractTerms())
.property("amount", codecForAmountString()) .property("exchangeBaseUrl", codecForString())
.property("exchangeBaseUrl", codecForAmountString())
.build("InitiatePeerPullPaymentRequest"); .build("InitiatePeerPullPaymentRequest");
export interface InitiatePeerPullPaymentResponse { export interface InitiatePeerPullPaymentResponse {

View File

@ -27,7 +27,6 @@ import { SynchronousCryptoWorker } from "./synchronousWorkerNode.js";
*/ */
export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory {
startWorker(): CryptoWorker { startWorker(): CryptoWorker {
return new SynchronousCryptoWorker(); return new SynchronousCryptoWorker();
} }

View File

@ -26,7 +26,7 @@ import {
BackupRefreshReason, BackupRefreshReason,
BackupRefundState, BackupRefundState,
BackupWgType, BackupWgType,
codecForContractTerms, codecForMerchantContractTerms,
CoinStatus, CoinStatus,
DenomKeyType, DenomKeyType,
DenomSelectionState, DenomSelectionState,
@ -638,7 +638,7 @@ export async function importBackup(
break; break;
} }
} }
const parsedContractTerms = codecForContractTerms().decode( const parsedContractTerms = codecForMerchantContractTerms().decode(
backupPurchase.contract_terms_raw, backupPurchase.contract_terms_raw,
); );
const amount = Amounts.parseOrThrow(parsedContractTerms.amount); const amount = Amounts.parseOrThrow(parsedContractTerms.amount);

View File

@ -34,7 +34,7 @@ import {
Amounts, Amounts,
ApplyRefundResponse, ApplyRefundResponse,
codecForAbortResponse, codecForAbortResponse,
codecForContractTerms, codecForMerchantContractTerms,
codecForMerchantOrderRefundPickupResponse, codecForMerchantOrderRefundPickupResponse,
codecForMerchantOrderStatusPaid, codecForMerchantOrderStatusPaid,
codecForMerchantPayResponse, codecForMerchantPayResponse,
@ -456,7 +456,7 @@ export async function processDownloadProposal(
let parsedContractTerms: MerchantContractTerms; let parsedContractTerms: MerchantContractTerms;
try { try {
parsedContractTerms = codecForContractTerms().decode( parsedContractTerms = codecForMerchantContractTerms().decode(
proposalResp.contract_terms, proposalResp.contract_terms,
); );
} catch (e) { } catch (e) {
@ -1584,7 +1584,7 @@ export async function runPayForConfirmPay(
const numRetry = opRetry?.retryInfo.retryCounter ?? 0; const numRetry = opRetry?.retryInfo.retryCounter ?? 0;
if ( if (
res.errorDetail.code === res.errorDetail.code ===
TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
numRetry < maxRetry numRetry < maxRetry
) { ) {
// Pretend the operation is pending instead of reporting // Pretend the operation is pending instead of reporting

View File

@ -57,6 +57,10 @@ import {
parsePayPullUri, parsePayPullUri,
parsePayPushUri, parsePayPushUri,
PeerContractTerms, PeerContractTerms,
PreparePeerPullPaymentRequest,
PreparePeerPullPaymentResponse,
PreparePeerPushPaymentRequest,
PreparePeerPushPaymentResponse,
RefreshReason, RefreshReason,
strcmp, strcmp,
TalerProtocolTimestamp, TalerProtocolTimestamp,
@ -218,28 +222,30 @@ export async function selectPeerCoins(
return undefined; return undefined;
} }
export async function preparePeerPushPayment(
ws: InternalWalletState,
req: PreparePeerPushPaymentRequest,
): Promise<PreparePeerPushPaymentResponse> {
// FIXME: look up for the exchange and calculate fee
return {
amountEffective: req.amount,
amountRaw: req.amount,
};
}
export async function initiatePeerToPeerPush( export async function initiatePeerToPeerPush(
ws: InternalWalletState, ws: InternalWalletState,
req: InitiatePeerPushPaymentRequest, req: InitiatePeerPushPaymentRequest,
): Promise<InitiatePeerPushPaymentResponse> { ): Promise<InitiatePeerPushPaymentResponse> {
const instructedAmount = Amounts.parseOrThrow(req.amount); const instructedAmount = Amounts.parseOrThrow(
req.partialContractTerms.amount,
);
const purseExpiration = req.partialContractTerms.purse_expiration;
const contractTerms = req.partialContractTerms;
const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({});
const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
Duration.fromSpec({ days: 2 }),
),
);
const contractTerms = {
...req.partialContractTerms,
purse_expiration: purseExpiration,
amount: req.amount,
};
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
const econtractResp = await ws.cryptoApi.encryptContractForMerge({ const econtractResp = await ws.cryptoApi.encryptContractForMerge({
@ -751,6 +757,16 @@ export async function checkPeerPullPayment(
}; };
} }
export async function preparePeerPullPayment(
ws: InternalWalletState,
req: PreparePeerPullPaymentRequest,
): Promise<PreparePeerPullPaymentResponse> {
//FIXME: look up for exchange details and use purse fee
return {
amountEffective: req.amount,
amountRaw: req.amount,
};
}
/** /**
* Initiate a peer pull payment. * Initiate a peer pull payment.
*/ */
@ -769,24 +785,17 @@ export async function initiatePeerPullPayment(
const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({});
const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( const instructedAmount = Amounts.parseOrThrow(
AbsoluteTime.addDuration( req.partialContractTerms.amount,
AbsoluteTime.now(),
Duration.fromSpec({ days: 2 }),
),
); );
const purseExpiration = req.partialContractTerms.purse_expiration;
const contractTerms = req.partialContractTerms;
const reservePayto = talerPaytoFromExchangeReserve( const reservePayto = talerPaytoFromExchangeReserve(
req.exchangeBaseUrl, req.exchangeBaseUrl,
mergeReserveInfo.reservePub, mergeReserveInfo.reservePub,
); );
const contractTerms = {
...req.partialContractTerms,
amount: req.amount,
purse_expiration: purseExpiration,
};
const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
contractTerms, contractTerms,
pursePriv: pursePair.priv, pursePriv: pursePair.priv,
@ -796,7 +805,7 @@ export async function initiatePeerPullPayment(
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
const purseFee = Amounts.stringify( const purseFee = Amounts.stringify(
Amounts.zeroOfCurrency(Amounts.parseOrThrow(req.amount).currency), Amounts.zeroOfCurrency(instructedAmount.currency),
); );
const sigRes = await ws.cryptoApi.signReservePurseCreate({ const sigRes = await ws.cryptoApi.signReservePurseCreate({
@ -804,7 +813,7 @@ export async function initiatePeerPullPayment(
flags: WalletAccountMergeFlags.CreateWithPurseFee, flags: WalletAccountMergeFlags.CreateWithPurseFee,
mergePriv: mergePair.priv, mergePriv: mergePair.priv,
mergeTimestamp: mergeTimestamp, mergeTimestamp: mergeTimestamp,
purseAmount: req.amount, purseAmount: req.partialContractTerms.amount,
purseExpiration: purseExpiration, purseExpiration: purseExpiration,
purseFee: purseFee, purseFee: purseFee,
pursePriv: pursePair.priv, pursePriv: pursePair.priv,
@ -817,7 +826,7 @@ export async function initiatePeerPullPayment(
.mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.peerPullPaymentInitiations.put({ await tx.peerPullPaymentInitiations.put({
amount: req.amount, amount: req.partialContractTerms.amount,
contractTermsHash: hContractTerms, contractTermsHash: hContractTerms,
exchangeBaseUrl: req.exchangeBaseUrl, exchangeBaseUrl: req.exchangeBaseUrl,
pursePriv: pursePair.priv, pursePriv: pursePair.priv,
@ -840,7 +849,7 @@ export async function initiatePeerPullPayment(
purse_fee: purseFee, purse_fee: purseFee,
purse_pub: pursePair.pub, purse_pub: pursePair.pub,
purse_sig: sigRes.purseSig, purse_sig: sigRes.purseSig,
purse_value: req.amount, purse_value: req.partialContractTerms.amount,
reserve_sig: sigRes.accountSig, reserve_sig: sigRes.accountSig,
econtract: econtractResp.econtract, econtract: econtractResp.econtract,
}; };
@ -862,7 +871,7 @@ export async function initiatePeerPullPayment(
logger.info(`reserve merge response: ${j2s(resp)}`); logger.info(`reserve merge response: ${j2s(resp)}`);
const wg = await internalCreateWithdrawalGroup(ws, { const wg = await internalCreateWithdrawalGroup(ws, {
amount: Amounts.parseOrThrow(req.amount), amount: instructedAmount,
wgInfo: { wgInfo: {
withdrawalType: WithdrawalRecordType.PeerPullCredit, withdrawalType: WithdrawalRecordType.PeerPullCredit,
contractTerms, contractTerms,

View File

@ -75,6 +75,10 @@ import {
PrepareDepositResponse, PrepareDepositResponse,
PreparePayRequest, PreparePayRequest,
PreparePayResult, PreparePayResult,
PreparePeerPullPaymentRequest,
PreparePeerPullPaymentResponse,
PreparePeerPushPaymentRequest,
PreparePeerPushPaymentResponse,
PrepareRefundRequest, PrepareRefundRequest,
PrepareRefundResult, PrepareRefundResult,
PrepareTipRequest, PrepareTipRequest,
@ -164,9 +168,11 @@ export enum WalletApiOperation {
WithdrawFakebank = "withdrawFakebank", WithdrawFakebank = "withdrawFakebank",
ImportDb = "importDb", ImportDb = "importDb",
ExportDb = "exportDb", ExportDb = "exportDb",
PreparePeerPushPayment = "preparePeerPushPayment",
InitiatePeerPushPayment = "initiatePeerPushPayment", InitiatePeerPushPayment = "initiatePeerPushPayment",
CheckPeerPushPayment = "checkPeerPushPayment", CheckPeerPushPayment = "checkPeerPushPayment",
AcceptPeerPushPayment = "acceptPeerPushPayment", AcceptPeerPushPayment = "acceptPeerPushPayment",
PreparePeerPullPayment = "preparePeerPullPayment",
InitiatePeerPullPayment = "initiatePeerPullPayment", InitiatePeerPullPayment = "initiatePeerPullPayment",
CheckPeerPullPayment = "checkPeerPullPayment", CheckPeerPullPayment = "checkPeerPullPayment",
AcceptPeerPullPayment = "acceptPeerPullPayment", AcceptPeerPullPayment = "acceptPeerPullPayment",
@ -553,6 +559,15 @@ export type ExportBackupPlainOp = {
// group: Peer Payments // group: Peer Payments
/**
* Initiate an outgoing peer push payment.
*/
export type PreparePeerPushPaymentOp = {
op: WalletApiOperation.PreparePeerPushPayment;
request: PreparePeerPushPaymentRequest;
response: PreparePeerPushPaymentResponse;
};
/** /**
* Initiate an outgoing peer push payment. * Initiate an outgoing peer push payment.
*/ */
@ -580,6 +595,15 @@ export type AcceptPeerPushPaymentOp = {
response: EmptyObject; response: EmptyObject;
}; };
/**
* Initiate an outgoing peer pull payment.
*/
export type PreparePeerPullPaymentOp = {
op: WalletApiOperation.PreparePeerPullPayment;
request: PreparePeerPullPaymentRequest;
response: PreparePeerPullPaymentResponse;
};
/** /**
* Initiate an outgoing peer pull payment. * Initiate an outgoing peer pull payment.
*/ */
@ -815,9 +839,11 @@ export type WalletOperations = {
[WalletApiOperation.TestPay]: TestPayOp; [WalletApiOperation.TestPay]: TestPayOp;
[WalletApiOperation.ExportDb]: ExportDbOp; [WalletApiOperation.ExportDb]: ExportDbOp;
[WalletApiOperation.ImportDb]: ImportDbOp; [WalletApiOperation.ImportDb]: ImportDbOp;
[WalletApiOperation.PreparePeerPushPayment]: PreparePeerPushPaymentOp;
[WalletApiOperation.InitiatePeerPushPayment]: InitiatePeerPushPaymentOp; [WalletApiOperation.InitiatePeerPushPayment]: InitiatePeerPushPaymentOp;
[WalletApiOperation.CheckPeerPushPayment]: CheckPeerPushPaymentOp; [WalletApiOperation.CheckPeerPushPayment]: CheckPeerPushPaymentOp;
[WalletApiOperation.AcceptPeerPushPayment]: AcceptPeerPushPaymentOp; [WalletApiOperation.AcceptPeerPushPayment]: AcceptPeerPushPaymentOp;
[WalletApiOperation.PreparePeerPullPayment]: PreparePeerPullPaymentOp;
[WalletApiOperation.InitiatePeerPullPayment]: InitiatePeerPullPaymentOp; [WalletApiOperation.InitiatePeerPullPayment]: InitiatePeerPullPaymentOp;
[WalletApiOperation.CheckPeerPullPayment]: CheckPeerPullPaymentOp; [WalletApiOperation.CheckPeerPullPayment]: CheckPeerPullPaymentOp;
[WalletApiOperation.AcceptPeerPullPayment]: AcceptPeerPullPaymentOp; [WalletApiOperation.AcceptPeerPullPayment]: AcceptPeerPullPaymentOp;

View File

@ -57,6 +57,8 @@ import {
codecForListKnownBankAccounts, codecForListKnownBankAccounts,
codecForPrepareDepositRequest, codecForPrepareDepositRequest,
codecForPreparePayRequest, codecForPreparePayRequest,
codecForPreparePeerPullPaymentRequest,
codecForPreparePeerPushPaymentRequest,
codecForPrepareRefundRequest, codecForPrepareRefundRequest,
codecForPrepareTipRequest, codecForPrepareTipRequest,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
@ -186,6 +188,8 @@ import {
checkPeerPushPayment, checkPeerPushPayment,
initiatePeerPullPayment, initiatePeerPullPayment,
initiatePeerToPeerPush, initiatePeerToPeerPush,
preparePeerPullPayment,
preparePeerPushPayment,
} from "./operations/pay-peer.js"; } from "./operations/pay-peer.js";
import { getPendingOperations } from "./operations/pending.js"; import { getPendingOperations } from "./operations/pending.js";
import { import {
@ -659,7 +663,9 @@ async function getExchanges(
const opRetryRecord = await tx.operationRetries.get( const opRetryRecord = await tx.operationRetries.get(
RetryTags.forExchangeUpdate(r), RetryTags.forExchangeUpdate(r),
); );
exchanges.push(makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError)); exchanges.push(
makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
);
} }
}); });
return { exchanges }; return { exchanges };
@ -927,9 +933,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof, ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation spend_allocation: c.spendAllocation
? { ? {
amount: c.spendAllocation.amount, amount: c.spendAllocation.amount,
id: c.spendAllocation.id, id: c.spendAllocation.id,
} }
: undefined, : undefined,
}); });
} }
@ -1340,6 +1346,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await importDb(ws.db.idbHandle(), req.dump); await importDb(ws.db.idbHandle(), req.dump);
return []; return [];
} }
case WalletApiOperation.PreparePeerPushPayment: {
const req = codecForPreparePeerPushPaymentRequest().decode(payload);
return await preparePeerPushPayment(ws, req);
}
case WalletApiOperation.InitiatePeerPushPayment: { case WalletApiOperation.InitiatePeerPushPayment: {
const req = codecForInitiatePeerPushPaymentRequest().decode(payload); const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
return await initiatePeerToPeerPush(ws, req); return await initiatePeerToPeerPush(ws, req);
@ -1352,6 +1362,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const req = codecForAcceptPeerPushPaymentRequest().decode(payload); const req = codecForAcceptPeerPushPaymentRequest().decode(payload);
return await acceptPeerPushPayment(ws, req); return await acceptPeerPushPayment(ws, req);
} }
case WalletApiOperation.PreparePeerPullPayment: {
const req = codecForPreparePeerPullPaymentRequest().decode(payload);
return await preparePeerPullPayment(ws, req);
}
case WalletApiOperation.InitiatePeerPullPayment: { case WalletApiOperation.InitiatePeerPullPayment: {
const req = codecForInitiatePeerPullPaymentRequest().decode(payload); const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
return await initiatePeerPullPayment(ws, req); return await initiatePeerPullPayment(ws, req);

View File

@ -59,10 +59,10 @@ export namespace State {
doSelectExchange: ButtonHandler; doSelectExchange: ButtonHandler;
create: ButtonHandler; create: ButtonHandler;
subject: TextFieldHandler; subject: TextFieldHandler;
expiration: TextFieldHandler;
toBeReceived: AmountJson; toBeReceived: AmountJson;
chosenAmount: AmountJson; requestAmount: AmountJson;
exchangeUrl: string; exchangeUrl: string;
invalid: boolean;
error: undefined; error: undefined;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;
} }

View File

@ -15,8 +15,9 @@
*/ */
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
@ -49,7 +50,8 @@ export function useComponentState(
const exchangeList = hook.response.exchanges; const exchangeList = hook.response.exchanges;
return () => { return () => {
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>()
const [operationError, setOperationError] = useState< const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined TalerErrorDetail | undefined
@ -67,13 +69,59 @@ export function useComponentState(
const exchange = selectedExchange.selected; const exchange = selectedExchange.selected;
const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(WalletApiOperation.PreparePeerPullPayment, {
amount: amountStr,
exchangeBaseUrl: exchange.exchangeBaseUrl,
})
return resp
})
if (!hook) {
return {
status: "loading",
error: undefined
}
}
if (hook.hasError) {
return {
status: "loading-uri",
error: hook
}
}
const { amountEffective, amountRaw } = hook.response
const requestAmount = Amounts.parseOrThrow(amountRaw)
const toBeReceived = Amounts.parseOrThrow(amountEffective)
let purse_expiration: TalerProtocolTimestamp | undefined = undefined
let timestampError: string | undefined = undefined;
const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date())
if (t !== undefined) {
if (Number.isNaN(t.getTime())) {
timestampError = 'Should have the format "dd/MM/yyyy"'
} else {
if (!isFuture(t)) {
timestampError = 'Should be in the future'
} else {
purse_expiration = {
t_s: t.getTime() / 1000
}
}
}
}
async function accept(): Promise<void> { async function accept(): Promise<void> {
if (!subject || !purse_expiration) return;
try { try {
const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPullPayment, { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPullPayment, {
amount: Amounts.stringify(amount),
exchangeBaseUrl: exchange.exchangeBaseUrl, exchangeBaseUrl: exchange.exchangeBaseUrl,
partialContractTerms: { partialContractTerms: {
amount: Amounts.stringify(amount),
summary: subject, summary: subject,
purse_expiration
}, },
}); });
@ -86,25 +134,32 @@ export function useComponentState(
throw Error("error trying to accept"); throw Error("error trying to accept");
} }
} }
const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration
return { return {
status: "ready", status: "ready",
subject: { subject: {
error: !subject ? "cant be empty" : undefined, error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined,
value: subject, value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },
expiration: {
error: timestampError,
value: timestamp === undefined ? "" : timestamp,
onInput: async (e) => {
setTimestamp(e)
}
},
doSelectExchange: selectedExchange.doSelect, doSelectExchange: selectedExchange.doSelect,
invalid: !subject || Amounts.isZero(amount),
exchangeUrl: exchange.exchangeBaseUrl, exchangeUrl: exchange.exchangeBaseUrl,
create: { create: {
onClick: accept, onClick: unableToCreate ? undefined : accept,
}, },
cancel: { cancel: {
onClick: onClose, onClick: onClose,
}, },
chosenAmount: amount, requestAmount,
toBeReceived: amount, toBeReceived,
error: undefined, error: undefined,
operationError, operationError,
}; };

View File

@ -27,11 +27,14 @@ export default {
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {
chosenAmount: { requestAmount: {
currency: "ARS", currency: "ARS",
value: 1, value: 1,
fraction: 0, fraction: 0,
}, },
expiration: {
value: "2/12/12",
},
cancel: {}, cancel: {},
toBeReceived: { toBeReceived: {
currency: "ARS", currency: "ARS",

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { format } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
@ -46,18 +47,40 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
} }
export function ReadyView({ export function ReadyView({
invalid,
exchangeUrl, exchangeUrl,
subject, subject,
expiration,
cancel, cancel,
operationError, operationError,
create, create,
toBeReceived, toBeReceived,
chosenAmount, requestAmount,
doSelectExchange, doSelectExchange,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
async function oneDayExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"),
);
}
}
async function oneWeekExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"),
);
}
}
async function _20DaysExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
);
}
}
return ( return (
<WalletAction> <WalletAction>
<LogoHeader /> <LogoHeader />
@ -75,16 +98,6 @@ export function ReadyView({
/> />
)} )}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<TextField
label="Subject"
variant="filled"
error={subject.error}
required
fullWidth
value={subject.value}
onChange={subject.onInput}
/>
<Part <Part
title={ title={
<div <div
@ -107,6 +120,52 @@ export function ReadyView({
kind="neutral" kind="neutral"
big big
/> />
<p>
<TextField
label="Subject"
variant="filled"
error={subject.error}
required
fullWidth
value={subject.value}
onChange={subject.onInput}
/>
</p>
<p>
<TextField
label="Expiration"
variant="filled"
error={expiration.error}
required
fullWidth
value={expiration.value}
onChange={expiration.onInput}
/>
<p>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={oneDayExpiration}
>
1 day
</Button>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={oneWeekExpiration}
>
1 week
</Button>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={_20DaysExpiration}
>
20 days
</Button>
</p>
</p>
<Part <Part
title={<i18n.Translate>Details</i18n.Translate>} title={<i18n.Translate>Details</i18n.Translate>}
@ -114,19 +173,14 @@ export function ReadyView({
<InvoiceDetails <InvoiceDetails
amount={{ amount={{
effective: toBeReceived, effective: toBeReceived,
raw: chosenAmount, raw: requestAmount,
}} }}
/> />
} }
/> />
</section> </section>
<section> <section>
<Button <Button onClick={create.onClick} variant="contained" color="success">
disabled={invalid}
onClick={create.onClick}
variant="contained"
color="success"
>
<i18n.Translate>Create</i18n.Translate> <i18n.Translate>Create</i18n.Translate>
</Button> </Button>
</section> </section>

View File

@ -48,11 +48,11 @@ export namespace State {
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
invalid: boolean;
create: ButtonHandler; create: ButtonHandler;
toBeReceived: AmountJson; toBeReceived: AmountJson;
chosenAmount: AmountJson; debitAmount: AmountJson;
subject: TextFieldHandler; subject: TextFieldHandler;
expiration: TextFieldHandler;
error: undefined; error: undefined;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;
} }

View File

@ -14,9 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { format, isFuture, parse } from "date-fns";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
@ -26,17 +28,65 @@ export function useComponentState(
): State { ): State {
const amount = Amounts.parseOrThrow(amountStr); const amount = Amounts.parseOrThrow(amountStr);
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>()
const [operationError, setOperationError] = useState< const [operationError, setOperationError] = useState<
TalerErrorDetail | undefined TalerErrorDetail | undefined
>(undefined); >(undefined);
const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(WalletApiOperation.PreparePeerPushPayment, {
amount: amountStr
})
return resp
})
if (!hook) {
return {
status: "loading",
error: undefined
}
}
if (hook.hasError) {
return {
status: "loading-uri",
error: hook
}
}
const { amountEffective, amountRaw } = hook.response
const debitAmount = Amounts.parseOrThrow(amountRaw)
const toBeReceived = Amounts.parseOrThrow(amountEffective)
let purse_expiration: TalerProtocolTimestamp | undefined = undefined
let timestampError: string | undefined = undefined;
const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date())
if (t !== undefined) {
if (Number.isNaN(t.getTime())) {
timestampError = 'Should have the format "dd/MM/yyyy"'
} else {
if (!isFuture(t)) {
timestampError = 'Should be in the future'
} else {
purse_expiration = {
t_s: t.getTime() / 1000
}
}
}
}
async function accept(): Promise<void> { async function accept(): Promise<void> {
if (!subject || !purse_expiration) return;
try { try {
const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, {
amount: Amounts.stringify(amount),
partialContractTerms: { partialContractTerms: {
summary: subject, summary: subject,
amount: amountStr,
purse_expiration
}, },
}); });
onSuccess(resp.transactionId); onSuccess(resp.transactionId);
@ -48,22 +98,31 @@ export function useComponentState(
throw Error("error trying to accept"); throw Error("error trying to accept");
} }
} }
const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration
return { return {
status: "ready", status: "ready",
invalid: !subject || Amounts.isZero(amount),
cancel: { cancel: {
onClick: onClose, onClick: onClose,
}, },
subject: { subject: {
error: !subject ? "cant be empty" : undefined, error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined,
value: subject, value: subject ?? "",
onInput: async (e) => setSubject(e), onInput: async (e) => setSubject(e),
}, },
create: { expiration: {
onClick: accept, error: timestampError,
value: timestamp === undefined ? "" : timestamp,
onInput: async (e) => {
setTimestamp(e)
}
}, },
chosenAmount: amount, create: {
toBeReceived: amount, onClick: unableToCreate ? undefined : accept,
},
debitAmount,
toBeReceived,
error: undefined, error: undefined,
operationError, operationError,
}; };

View File

@ -27,11 +27,14 @@ export default {
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {
chosenAmount: { debitAmount: {
currency: "ARS", currency: "ARS",
value: 1, value: 1,
fraction: 0, fraction: 0,
}, },
expiration: {
value: "20/1/2022",
},
create: {}, create: {},
cancel: {}, cancel: {},
toBeReceived: { toBeReceived: {

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { format } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
@ -40,14 +41,37 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ export function ReadyView({
subject, subject,
expiration,
toBeReceived, toBeReceived,
chosenAmount, debitAmount,
create, create,
operationError, operationError,
cancel, cancel,
invalid,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
async function oneDayExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"),
);
}
}
async function oneWeekExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"),
);
}
}
async function _20DaysExpiration() {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
);
}
}
return ( return (
<WalletAction> <WalletAction>
<LogoHeader /> <LogoHeader />
@ -65,34 +89,65 @@ export function ReadyView({
/> />
)} )}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<TextField <p>
label="Subject" <TextField
variant="filled" label="Subject"
error={subject.error} variant="filled"
required error={subject.error}
fullWidth required
value={subject.value} fullWidth
onChange={subject.onInput} value={subject.value}
/> onChange={subject.onInput}
/>
</p>
<p>
<TextField
label="Expiration"
variant="filled"
error={expiration.error}
required
fullWidth
value={expiration.value}
onChange={expiration.onInput}
/>
<p>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={oneDayExpiration}
>
1 day
</Button>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={oneWeekExpiration}
>
1 week
</Button>
<Button
variant="outlined"
disabled={!expiration.onInput}
onClick={_20DaysExpiration}
>
20 days
</Button>
</p>
</p>
<Part <Part
title={<i18n.Translate>Details</i18n.Translate>} title={<i18n.Translate>Details</i18n.Translate>}
text={ text={
<TransferDetails <TransferDetails
amount={{ amount={{
effective: toBeReceived, effective: toBeReceived,
raw: chosenAmount, raw: debitAmount,
}} }}
/> />
} }
/> />
</section> </section>
<section> <section>
<Button <Button onClick={create.onClick} variant="contained" color="success">
disabled={invalid}
onClick={create.onClick}
variant="contained"
color="success"
>
<i18n.Translate>Create</i18n.Translate> <i18n.Translate>Create</i18n.Translate>
</Button> </Button>
</section> </section>

View File

@ -290,7 +290,7 @@ export function Button({
return ( return (
<ButtonBase <ButtonBase
disabled={disabled || running} disabled={disabled || running || !doClick}
class={[ class={[
theme.typography.button, theme.typography.button,
theme.shape.roundBorder, theme.shape.roundBorder,