feature: 7440 add expiration to p2p
This commit is contained in:
parent
43c7cff750
commit
5c742afbdf
@ -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,
|
||||||
|
@ -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> =>
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user