aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-08-09 15:00:45 +0200
committerFlorian Dold <florian@dold.me>2022-08-16 17:55:12 +0200
commitac8f116780a860c8f4acfdf5553bf90d76afe236 (patch)
tree38abecb5ad3a3660161909ee9ca229d4ce08eb4a /packages/taler-util
parentfb8372dfbf27b7b4e8b2fe4f81aa2ba18bfcf638 (diff)
downloadwallet-core-ac8f116780a860c8f4acfdf5553bf90d76afe236.tar.xz
implement peer to peer push payments
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/src/codec.ts4
-rw-r--r--packages/taler-util/src/contractTerms.test.ts5
-rw-r--r--packages/taler-util/src/talerCrypto.test.ts22
-rw-r--r--packages/taler-util/src/taleruri.test.ts43
-rw-r--r--packages/taler-util/src/taleruri.ts57
-rw-r--r--packages/taler-util/src/time.ts8
-rw-r--r--packages/taler-util/src/walletTypes.ts28
7 files changed, 135 insertions, 32 deletions
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<GetContractTermsDetailsRequest> =>
- buildCodecForObject<GetContractTermsDetailsRequest>()
- .property("proposalId", codecForString())
- .build("GetContractTermsDetails");
+export const codecForGetContractTermsDetails =
+ (): Codec<GetContractTermsDetailsRequest> =>
+ buildCodecForObject<GetContractTermsDetailsRequest>()
+ .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<CheckPeerPushPaymentRequest> =>
buildCodecForObject<CheckPeerPushPaymentRequest>()
- .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<AcceptPeerPushPaymentRequest> =>
buildCodecForObject<AcceptPeerPushPaymentRequest>()
- .property("pursePub", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("peerPushPaymentIncomingId", codecForString())
.build("AcceptPeerPushPaymentRequest");