From ac8f116780a860c8f4acfdf5553bf90d76afe236 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 9 Aug 2022 15:00:45 +0200 Subject: implement peer to peer push payments --- packages/taler-util/src/codec.ts | 4 +- packages/taler-util/src/contractTerms.test.ts | 5 +++ packages/taler-util/src/talerCrypto.test.ts | 22 ++++------- packages/taler-util/src/taleruri.test.ts | 43 ++++++++++++++++++++ packages/taler-util/src/taleruri.ts | 57 ++++++++++++++++++++++++++- packages/taler-util/src/time.ts | 8 ++++ packages/taler-util/src/walletTypes.ts | 28 ++++++------- 7 files changed, 135 insertions(+), 32 deletions(-) (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts index 2ea64a249..02e6a8830 100644 --- a/packages/taler-util/src/codec.ts +++ b/packages/taler-util/src/codec.ts @@ -186,7 +186,7 @@ class UnionCodecBuilder< throw new DecodingError( `expected tag for ${objectDisplayName} at ${renderContext( c, - )}.${discriminator}`, + )}.${String(discriminator)}`, ); } const alt = alternatives.get(d); @@ -194,7 +194,7 @@ class UnionCodecBuilder< throw new DecodingError( `unknown tag for ${objectDisplayName} ${d} at ${renderContext( c, - )}.${discriminator}`, + )}.${String(discriminator)}`, ); } const altDecoded = alt.codec.decode(x); diff --git a/packages/taler-util/src/contractTerms.test.ts b/packages/taler-util/src/contractTerms.test.ts index 74cae4ca7..d021495d0 100644 --- a/packages/taler-util/src/contractTerms.test.ts +++ b/packages/taler-util/src/contractTerms.test.ts @@ -18,8 +18,13 @@ * Imports. */ import test from "ava"; +import { initNodePrng } from "./prng-node.js"; import { ContractTermsUtil } from "./contractTerms.js"; +// Since we import nacl-fast directly (and not via index.node.ts), we need to +// init the PRNG manually. +initNodePrng(); + test("contract terms canon hashing", (t) => { const cReq = { foo: 42, diff --git a/packages/taler-util/src/talerCrypto.test.ts b/packages/taler-util/src/talerCrypto.test.ts index b4a0106fa..aa1873c7e 100644 --- a/packages/taler-util/src/talerCrypto.test.ts +++ b/packages/taler-util/src/talerCrypto.test.ts @@ -381,7 +381,7 @@ test("taler age restriction crypto", async (t) => { const pub2Ref = await Edx25519.getPublic(priv2); - t.is(pub2, pub2Ref); + t.deepEqual(pub2, pub2Ref); }); test("edx signing", async (t) => { @@ -390,21 +390,13 @@ test("edx signing", async (t) => { const msg = stringToBytes("hello world"); - const sig = nacl.crypto_edx25519_sign_detached( - msg, - priv1, - pub1, - ); + const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1); - t.true( - nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1), - ); + t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); sig[0]++; - t.false( - nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1), - ); + t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1)); }); test("edx test vector", async (t) => { @@ -422,18 +414,18 @@ test("edx test vector", async (t) => { { const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx)); - t.is(pub1Prime, decodeCrock(tv.pub1_edx)); + t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx)); } const pub2Prime = await Edx25519.publicKeyDerive( decodeCrock(tv.pub1_edx), decodeCrock(tv.seed), ); - t.is(pub2Prime, decodeCrock(tv.pub2_edx)); + t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx)); const priv2Prime = await Edx25519.privateKeyDerive( decodeCrock(tv.priv1_edx), decodeCrock(tv.seed), ); - t.is(priv2Prime, decodeCrock(tv.priv2_edx)); + t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx)); }); diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index 5bf7ad4ee..3ee243fb3 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -20,6 +20,8 @@ import { parseWithdrawUri, parseRefundUri, parseTipUri, + parsePayPushUri, + constructPayPushUri, } from "./taleruri.js"; test("taler pay url parsing: wrong scheme", (t) => { @@ -182,3 +184,44 @@ test("taler tip pickup uri with instance and prefix", (t) => { t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/"); t.is(r1.merchantTipId, "tipid"); }); + +test("taler peer to peer push URI", (t) => { + const url1 = "taler://pay-push/exch.example.com/foo"; + const r1 = parsePayPushUri(url1); + if (!r1) { + t.fail(); + return; + } + t.is(r1.exchangeBaseUrl, "https://exch.example.com/"); + t.is(r1.contractPriv, "foo"); +}); + +test("taler peer to peer push URI (path)", (t) => { + const url1 = "taler://pay-push/exch.example.com:123/bla/foo"; + const r1 = parsePayPushUri(url1); + if (!r1) { + t.fail(); + return; + } + t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/"); + t.is(r1.contractPriv, "foo"); +}); + +test("taler peer to peer push URI (http)", (t) => { + const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo"; + const r1 = parsePayPushUri(url1); + if (!r1) { + t.fail(); + return; + } + t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/"); + t.is(r1.contractPriv, "foo"); +}); + +test("taler peer to peer push URI (construction)", (t) => { + const url = constructPayPushUri({ + exchangeBaseUrl: "https://foo.example.com/bla/", + contractPriv: "123", + }); + t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123"); +}); diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index b487c73ae..e3bd120f0 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -15,7 +15,7 @@ */ import { canonicalizeBaseUrl } from "./helpers.js"; -import { URLSearchParams } from "./url.js"; +import { URLSearchParams, URL } from "./url.js"; export interface PayUriResult { merchantBaseUrl: string; @@ -40,6 +40,11 @@ export interface TipUriResult { merchantBaseUrl: string; } +export interface PayPushUriResult { + exchangeBaseUrl: string; + contractPriv: string; +} + /** * Parse a taler[+http]://withdraw URI. * Return undefined if not passed a valid URI. @@ -79,6 +84,7 @@ export enum TalerUriType { TalerTip = "taler-tip", TalerRefund = "taler-refund", TalerNotifyReserve = "taler-notify-reserve", + TalerPayPush = "pay-push", Unknown = "unknown", } @@ -111,6 +117,12 @@ export function classifyTalerUri(s: string): TalerUriType { if (sl.startsWith("taler+http://withdraw/")) { return TalerUriType.TalerWithdraw; } + if (sl.startsWith("taler://pay-push/")) { + return TalerUriType.TalerPayPush; + } + if (sl.startsWith("taler+http://pay-push/")) { + return TalerUriType.TalerPayPush; + } if (sl.startsWith("taler://notify-reserve/")) { return TalerUriType.TalerNotifyReserve; } @@ -176,6 +188,28 @@ export function parsePayUri(s: string): PayUriResult | undefined { }; } +export function parsePayPushUri(s: string): PayPushUriResult | undefined { + const pi = parseProtoInfo(s, "pay-push"); + if (!pi) { + return undefined; + } + const c = pi?.rest.split("?"); + const parts = c[0].split("/"); + if (parts.length < 2) { + return undefined; + } + const host = parts[0].toLowerCase(); + const contractPriv = parts[parts.length - 1]; + const pathSegments = parts.slice(1, parts.length - 1); + const p = [host, ...pathSegments].join("/"); + const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); + + return { + exchangeBaseUrl, + contractPriv, + }; +} + /** * Parse a taler[+http]://tip URI. * Return undefined if not passed a valid URI. @@ -228,3 +262,24 @@ export function parseRefundUri(s: string): RefundUriResult | undefined { orderId, }; } + +export function constructPayPushUri(args: { + exchangeBaseUrl: string; + contractPriv: string; +}): string { + const url = new URL(args.exchangeBaseUrl); + let proto: string; + if (url.protocol === "https:") { + proto = "taler"; + } else if (url.protocol === "http:") { + proto = "taler+http"; + } else { + throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`); + } + if (!url.pathname.endsWith("/")) { + throw Error( + `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`, + ); + } + return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`; +} diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 8b0516bf8..0ba684beb 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -92,6 +92,14 @@ export namespace Duration { return { d_ms: deadline.t_ms - now.t_ms }; } + export function max(d1: Duration, d2: Duration): Duration { + return durationMax(d1, d2); + } + + export function min(d1: Duration, d2: Duration): Duration { + return durationMin(d1, d2); + } + export function toIntegerYears(d: Duration): number { if (typeof d.d_ms !== "number") { throw Error("infinite duration"); diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 9f7ba417a..eac9cf7db 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -858,10 +858,11 @@ interface GetContractTermsDetailsRequest { proposalId: string; } -export const codecForGetContractTermsDetails = (): Codec => - buildCodecForObject() - .property("proposalId", codecForString()) - .build("GetContractTermsDetails"); +export const codecForGetContractTermsDetails = + (): Codec => + buildCodecForObject() + .property("proposalId", codecForString()) + .build("GetContractTermsDetails"); export interface PreparePayRequest { talerPayUri: string; @@ -1280,6 +1281,7 @@ export interface InitiatePeerPushPaymentResponse { pursePub: string; mergePriv: string; contractPriv: string; + talerUri: string; } export const codecForInitiatePeerPushPaymentRequest = @@ -1290,32 +1292,30 @@ export const codecForInitiatePeerPushPaymentRequest = .build("InitiatePeerPushPaymentRequest"); export interface CheckPeerPushPaymentRequest { - exchangeBaseUrl: string; - pursePub: string; - contractPriv: string; + talerUri: string; } export interface CheckPeerPushPaymentResponse { contractTerms: any; amount: AmountString; + peerPushPaymentIncomingId: string; } export const codecForCheckPeerPushPaymentRequest = (): Codec => buildCodecForObject() - .property("pursePub", codecForString()) - .property("contractPriv", codecForString()) - .property("exchangeBaseUrl", codecForString()) + .property("talerUri", codecForString()) .build("CheckPeerPushPaymentRequest"); export interface AcceptPeerPushPaymentRequest { - exchangeBaseUrl: string; - pursePub: string; + /** + * Transparent identifier of the incoming peer push payment. + */ + peerPushPaymentIncomingId: string; } export const codecForAcceptPeerPushPaymentRequest = (): Codec => buildCodecForObject() - .property("pursePub", codecForString()) - .property("exchangeBaseUrl", codecForString()) + .property("peerPushPaymentIncomingId", codecForString()) .build("AcceptPeerPushPaymentRequest"); -- cgit v1.2.3