@eupi/taler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @eupi/taler
|
|
2
|
+
|
|
3
|
+
Top up GNU Taler reserves with standard European payment QR codes.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
A Taler wallet is funded by wiring money to the exchange's bank account with the
|
|
8
|
+
**reserve public key** as the transfer subject. The exchange watches its account
|
|
9
|
+
(via LibEuFin/Nexus), finds the key in the subject of an incoming credit, and
|
|
10
|
+
credits that reserve.
|
|
11
|
+
|
|
12
|
+
This package encodes that flow as an **EPC069-12 QR code** (the "EPC QR" / GiroCode
|
|
13
|
+
that many European banking apps scan natively). The QR carries the exchange's IBAN,
|
|
14
|
+
an optional amount, and the reserve public key as remittance text. The payer scans
|
|
15
|
+
it in their own banking app and confirms; since October 2025 the resulting SEPA
|
|
16
|
+
Instant Credit Transfer reaches the exchange in seconds. No Taler-specific software
|
|
17
|
+
is needed on the payer's side, and no payment service provider sits in between.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Taler wallet payer's own banking app exchange bank account
|
|
21
|
+
reserve keypair ──QR code──► scan, confirm transfer ──SCT──► subject: <reserve pub>
|
|
22
|
+
│
|
|
23
|
+
coins withdrawn ◄─────────── exchange credits reserve ◄────── LibEuFin/Nexus
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { encodeTalerTopupQr, parseTalerTopupQr, findReservePub } from "@eupi/taler";
|
|
30
|
+
import QRCode from "qrcode";
|
|
31
|
+
|
|
32
|
+
// Wallet or exchange side: build the QR for a withdrawal
|
|
33
|
+
const payload = encodeTalerTopupQr({
|
|
34
|
+
accountName: "Taler Exchange Demo",
|
|
35
|
+
iban: "DE71 1102 2033 0123 4567 89",
|
|
36
|
+
amount: 50, // omit for an open-amount QR
|
|
37
|
+
reservePub: "ABCDEFGHJKMNPQRSTVWXYZ0123456789ABCDEFGHJKMNPQRSTVWX",
|
|
38
|
+
});
|
|
39
|
+
await QRCode.toDataURL(payload, { errorCorrectionLevel: "M" });
|
|
40
|
+
|
|
41
|
+
// Integration side: recognise top-up QR codes
|
|
42
|
+
const topup = parseTalerTopupQr(scannedText);
|
|
43
|
+
// topup.reservePub, topup.iban, topup.amount
|
|
44
|
+
|
|
45
|
+
// Bank-watcher side: match incoming transfer subjects
|
|
46
|
+
const pub = findReservePub("Taler ABCDEFGH... withdrawal");
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Validation is inherited from [`@eupi/qr`](../qr) (IBAN check digits, amount range,
|
|
50
|
+
payload limits) plus Taler reserve key checks (52-character Crockford base32 in
|
|
51
|
+
Taler's alphabet: 0-9, A-Z without I, L, O, U). `findReservePub` tolerates
|
|
52
|
+
surrounding text, since banks may decorate the subject line.
|
|
53
|
+
|
|
54
|
+
## Roadmap
|
|
55
|
+
|
|
56
|
+
A runnable end-to-end demonstration against libeufin-bank (create accounts, render
|
|
57
|
+
the QR, execute the scanned transfer, observe the reserve credit) is planned as the
|
|
58
|
+
next step, along with EN 18184 QR support offered upstream to the Taler wallets.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
Apache-2.0
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
EpcQrError: () => import_qr.EpcQrError,
|
|
24
|
+
RESERVE_PUB_RE: () => RESERVE_PUB_RE,
|
|
25
|
+
TalerTopupError: () => TalerTopupError,
|
|
26
|
+
encodeTalerTopupQr: () => encodeTalerTopupQr,
|
|
27
|
+
findReservePub: () => findReservePub,
|
|
28
|
+
isValidReservePub: () => isValidReservePub,
|
|
29
|
+
parseTalerTopupQr: () => parseTalerTopupQr
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var import_qr = require("@eupi/qr");
|
|
33
|
+
var RESERVE_PUB_RE = /^[0-9A-HJKMNP-TV-Z]{52}$/;
|
|
34
|
+
function isValidReservePub(input) {
|
|
35
|
+
return RESERVE_PUB_RE.test(input);
|
|
36
|
+
}
|
|
37
|
+
function findReservePub(subject) {
|
|
38
|
+
for (const token of subject.toUpperCase().split(/[^0-9A-Z]+/)) {
|
|
39
|
+
if (RESERVE_PUB_RE.test(token)) return token;
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
var TalerTopupError = class extends Error {
|
|
44
|
+
issues;
|
|
45
|
+
constructor(message, issues) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "TalerTopupError";
|
|
48
|
+
this.issues = issues;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function encodeTalerTopupQr(options) {
|
|
52
|
+
const reservePub = options.reservePub.toUpperCase();
|
|
53
|
+
if (!isValidReservePub(reservePub)) {
|
|
54
|
+
throw new TalerTopupError("invalid Taler reserve public key", [
|
|
55
|
+
{
|
|
56
|
+
field: "reservePub",
|
|
57
|
+
message: "must be 52 Crockford base32 characters (0-9, A-Z without I, L, O, U)"
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
return (0, import_qr.encodeEpcQr)({
|
|
62
|
+
name: options.accountName,
|
|
63
|
+
iban: options.iban,
|
|
64
|
+
text: reservePub,
|
|
65
|
+
...options.bic !== void 0 ? { bic: options.bic } : {},
|
|
66
|
+
...options.amount !== void 0 ? { amount: options.amount } : {},
|
|
67
|
+
...options.crlf !== void 0 ? { crlf: options.crlf } : {}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseTalerTopupQr(payload) {
|
|
71
|
+
const { data } = (0, import_qr.decodeEpcQr)(payload);
|
|
72
|
+
const subject = data.text ?? data.reference ?? "";
|
|
73
|
+
const reservePub = findReservePub(subject);
|
|
74
|
+
if (reservePub === void 0) {
|
|
75
|
+
throw new TalerTopupError("no Taler reserve public key in remittance information", [
|
|
76
|
+
{ field: "text", message: "expected a 52-character Crockford base32 reserve public key" }
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
reservePub,
|
|
81
|
+
iban: data.iban,
|
|
82
|
+
accountName: data.name,
|
|
83
|
+
...data.bic !== void 0 ? { bic: data.bic } : {},
|
|
84
|
+
...data.amount !== void 0 ? { amount: data.amount } : {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
88
|
+
0 && (module.exports = {
|
|
89
|
+
EpcQrError,
|
|
90
|
+
RESERVE_PUB_RE,
|
|
91
|
+
TalerTopupError,
|
|
92
|
+
encodeTalerTopupQr,
|
|
93
|
+
findReservePub,
|
|
94
|
+
isValidReservePub,
|
|
95
|
+
parseTalerTopupQr
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Bridge between GNU Taler and the European payment QR standards.\n *\n * A Taler wallet is funded by wiring money to the exchange's bank account\n * with the reserve public key as the transfer subject: the exchange watches\n * its account (via LibEuFin/Nexus), finds the reserve public key in the\n * subject of an incoming credit, and credits that reserve.\n *\n * This package encodes that flow as a standard EPC069-12 QR code (the\n * \"EPC QR\" / GiroCode many European banking apps scan natively): the QR\n * carries the exchange's IBAN, an optional amount, and the reserve public\n * key as remittance text. Scanning it in a regular banking app produces a\n * correctly addressed SEPA (Instant) Credit Transfer that funds the\n * reserve, with no Taler-specific software on the payer's side.\n */\n\nimport { EpcQrError, decodeEpcQr, encodeEpcQr } from \"@eupi/qr\";\n\n/**\n * Taler encodes binary data in Crockford base32 using the alphabet\n * 0-9 A-Z without I, L, O, U. A reserve public key is a 32-byte EdDSA\n * public key, which encodes to 52 characters.\n */\nexport const RESERVE_PUB_RE = /^[0-9A-HJKMNP-TV-Z]{52}$/;\n\n/** True when the input is a well-formed Taler reserve public key. */\nexport function isValidReservePub(input: string): boolean {\n return RESERVE_PUB_RE.test(input);\n}\n\n/**\n * Extracts the first well-formed reserve public key token from a transfer\n * subject. Exchanges tolerate surrounding text in the subject line, and\n * some banks prepend or append their own text; this scans token-wise.\n */\nexport function findReservePub(subject: string): string | undefined {\n for (const token of subject.toUpperCase().split(/[^0-9A-Z]+/)) {\n if (RESERVE_PUB_RE.test(token)) return token;\n }\n return undefined;\n}\n\nexport interface TalerTopupIssue {\n field: string;\n message: string;\n}\n\nexport class TalerTopupError extends Error {\n readonly issues: TalerTopupIssue[];\n constructor(message: string, issues: TalerTopupIssue[]) {\n super(message);\n this.name = \"TalerTopupError\";\n this.issues = issues;\n }\n}\n\nexport interface TalerTopupOptions {\n /** Account holder name of the exchange's bank account, up to 70 characters. */\n accountName: string;\n /** IBAN of the exchange's bank account. */\n iban: string;\n /** BIC of the exchange's bank; optional within the EEA. */\n bic?: string;\n /**\n * Amount in euro (number or numeric string). Omit for an open-amount QR\n * where the payer chooses the top-up amount in their banking app.\n */\n amount?: number | string;\n /** The Taler reserve public key to credit (52-character Crockford base32). */\n reservePub: string;\n /** Use CRLF as the payload element separator instead of LF. */\n crlf?: boolean;\n}\n\n/**\n * Builds an EPC069-12 QR payload that tops up the given Taler reserve when\n * scanned and confirmed in the payer's own banking app. Render it with any\n * QR library at error correction level M.\n *\n * @throws TalerTopupError on a malformed reserve public key; EpcQrError on\n * invalid bank data (propagated from @eupi/qr).\n */\nexport function encodeTalerTopupQr(options: TalerTopupOptions): string {\n const reservePub = options.reservePub.toUpperCase();\n if (!isValidReservePub(reservePub)) {\n throw new TalerTopupError(\"invalid Taler reserve public key\", [\n {\n field: \"reservePub\",\n message: \"must be 52 Crockford base32 characters (0-9, A-Z without I, L, O, U)\",\n },\n ]);\n }\n return encodeEpcQr({\n name: options.accountName,\n iban: options.iban,\n text: reservePub,\n ...(options.bic !== undefined ? { bic: options.bic } : {}),\n ...(options.amount !== undefined ? { amount: options.amount } : {}),\n ...(options.crlf !== undefined ? { crlf: options.crlf } : {}),\n });\n}\n\nexport interface TalerTopup {\n /** The reserve public key found in the remittance text. */\n reservePub: string;\n /** IBAN of the receiving (exchange) account. */\n iban: string;\n /** Account holder name of the receiving account. */\n accountName: string;\n bic?: string;\n /** Amount in euro as a numeric string, when the QR fixes one. */\n amount?: string;\n}\n\n/**\n * Parses an EPC069-12 payload and extracts the Taler top-up details.\n *\n * @throws EpcQrError when the payload is not a valid EPC QR;\n * TalerTopupError when it contains no reserve public key.\n */\nexport function parseTalerTopupQr(payload: string): TalerTopup {\n const { data } = decodeEpcQr(payload);\n const subject = data.text ?? data.reference ?? \"\";\n const reservePub = findReservePub(subject);\n if (reservePub === undefined) {\n throw new TalerTopupError(\"no Taler reserve public key in remittance information\", [\n { field: \"text\", message: \"expected a 52-character Crockford base32 reserve public key\" },\n ]);\n }\n return {\n reservePub,\n iban: data.iban,\n accountName: data.name,\n ...(data.bic !== undefined ? { bic: data.bic } : {}),\n ...(data.amount !== undefined ? { amount: data.amount } : {}),\n };\n}\n\nexport { EpcQrError };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,gBAAqD;AAO9C,IAAM,iBAAiB;AAGvB,SAAS,kBAAkB,OAAwB;AACxD,SAAO,eAAe,KAAK,KAAK;AAClC;AAOO,SAAS,eAAe,SAAqC;AAClE,aAAW,SAAS,QAAQ,YAAY,EAAE,MAAM,YAAY,GAAG;AAC7D,QAAI,eAAe,KAAK,KAAK,EAAG,QAAO;AAAA,EACzC;AACA,SAAO;AACT;AAOO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC;AAAA,EACT,YAAY,SAAiB,QAA2B;AACtD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AA4BO,SAAS,mBAAmB,SAAoC;AACrE,QAAM,aAAa,QAAQ,WAAW,YAAY;AAClD,MAAI,CAAC,kBAAkB,UAAU,GAAG;AAClC,UAAM,IAAI,gBAAgB,oCAAoC;AAAA,MAC5D;AAAA,QACE,OAAO;AAAA,QACP,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AAAA,EACH;AACA,aAAO,uBAAY;AAAA,IACjB,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,MAAM;AAAA,IACN,GAAI,QAAQ,QAAQ,SAAY,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IACxD,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,IACjE,GAAI,QAAQ,SAAS,SAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,EAC7D,CAAC;AACH;AAoBO,SAAS,kBAAkB,SAA6B;AAC7D,QAAM,EAAE,KAAK,QAAI,uBAAY,OAAO;AACpC,QAAM,UAAU,KAAK,QAAQ,KAAK,aAAa;AAC/C,QAAM,aAAa,eAAe,OAAO;AACzC,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI,gBAAgB,yDAAyD;AAAA,MACjF,EAAE,OAAO,QAAQ,SAAS,8DAA8D;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,EAC7D;AACF;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export { EpcQrError } from '@eupi/qr';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridge between GNU Taler and the European payment QR standards.
|
|
5
|
+
*
|
|
6
|
+
* A Taler wallet is funded by wiring money to the exchange's bank account
|
|
7
|
+
* with the reserve public key as the transfer subject: the exchange watches
|
|
8
|
+
* its account (via LibEuFin/Nexus), finds the reserve public key in the
|
|
9
|
+
* subject of an incoming credit, and credits that reserve.
|
|
10
|
+
*
|
|
11
|
+
* This package encodes that flow as a standard EPC069-12 QR code (the
|
|
12
|
+
* "EPC QR" / GiroCode many European banking apps scan natively): the QR
|
|
13
|
+
* carries the exchange's IBAN, an optional amount, and the reserve public
|
|
14
|
+
* key as remittance text. Scanning it in a regular banking app produces a
|
|
15
|
+
* correctly addressed SEPA (Instant) Credit Transfer that funds the
|
|
16
|
+
* reserve, with no Taler-specific software on the payer's side.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Taler encodes binary data in Crockford base32 using the alphabet
|
|
21
|
+
* 0-9 A-Z without I, L, O, U. A reserve public key is a 32-byte EdDSA
|
|
22
|
+
* public key, which encodes to 52 characters.
|
|
23
|
+
*/
|
|
24
|
+
declare const RESERVE_PUB_RE: RegExp;
|
|
25
|
+
/** True when the input is a well-formed Taler reserve public key. */
|
|
26
|
+
declare function isValidReservePub(input: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the first well-formed reserve public key token from a transfer
|
|
29
|
+
* subject. Exchanges tolerate surrounding text in the subject line, and
|
|
30
|
+
* some banks prepend or append their own text; this scans token-wise.
|
|
31
|
+
*/
|
|
32
|
+
declare function findReservePub(subject: string): string | undefined;
|
|
33
|
+
interface TalerTopupIssue {
|
|
34
|
+
field: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
declare class TalerTopupError extends Error {
|
|
38
|
+
readonly issues: TalerTopupIssue[];
|
|
39
|
+
constructor(message: string, issues: TalerTopupIssue[]);
|
|
40
|
+
}
|
|
41
|
+
interface TalerTopupOptions {
|
|
42
|
+
/** Account holder name of the exchange's bank account, up to 70 characters. */
|
|
43
|
+
accountName: string;
|
|
44
|
+
/** IBAN of the exchange's bank account. */
|
|
45
|
+
iban: string;
|
|
46
|
+
/** BIC of the exchange's bank; optional within the EEA. */
|
|
47
|
+
bic?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Amount in euro (number or numeric string). Omit for an open-amount QR
|
|
50
|
+
* where the payer chooses the top-up amount in their banking app.
|
|
51
|
+
*/
|
|
52
|
+
amount?: number | string;
|
|
53
|
+
/** The Taler reserve public key to credit (52-character Crockford base32). */
|
|
54
|
+
reservePub: string;
|
|
55
|
+
/** Use CRLF as the payload element separator instead of LF. */
|
|
56
|
+
crlf?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds an EPC069-12 QR payload that tops up the given Taler reserve when
|
|
60
|
+
* scanned and confirmed in the payer's own banking app. Render it with any
|
|
61
|
+
* QR library at error correction level M.
|
|
62
|
+
*
|
|
63
|
+
* @throws TalerTopupError on a malformed reserve public key; EpcQrError on
|
|
64
|
+
* invalid bank data (propagated from @eupi/qr).
|
|
65
|
+
*/
|
|
66
|
+
declare function encodeTalerTopupQr(options: TalerTopupOptions): string;
|
|
67
|
+
interface TalerTopup {
|
|
68
|
+
/** The reserve public key found in the remittance text. */
|
|
69
|
+
reservePub: string;
|
|
70
|
+
/** IBAN of the receiving (exchange) account. */
|
|
71
|
+
iban: string;
|
|
72
|
+
/** Account holder name of the receiving account. */
|
|
73
|
+
accountName: string;
|
|
74
|
+
bic?: string;
|
|
75
|
+
/** Amount in euro as a numeric string, when the QR fixes one. */
|
|
76
|
+
amount?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parses an EPC069-12 payload and extracts the Taler top-up details.
|
|
80
|
+
*
|
|
81
|
+
* @throws EpcQrError when the payload is not a valid EPC QR;
|
|
82
|
+
* TalerTopupError when it contains no reserve public key.
|
|
83
|
+
*/
|
|
84
|
+
declare function parseTalerTopupQr(payload: string): TalerTopup;
|
|
85
|
+
|
|
86
|
+
export { RESERVE_PUB_RE, type TalerTopup, TalerTopupError, type TalerTopupIssue, type TalerTopupOptions, encodeTalerTopupQr, findReservePub, isValidReservePub, parseTalerTopupQr };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export { EpcQrError } from '@eupi/qr';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridge between GNU Taler and the European payment QR standards.
|
|
5
|
+
*
|
|
6
|
+
* A Taler wallet is funded by wiring money to the exchange's bank account
|
|
7
|
+
* with the reserve public key as the transfer subject: the exchange watches
|
|
8
|
+
* its account (via LibEuFin/Nexus), finds the reserve public key in the
|
|
9
|
+
* subject of an incoming credit, and credits that reserve.
|
|
10
|
+
*
|
|
11
|
+
* This package encodes that flow as a standard EPC069-12 QR code (the
|
|
12
|
+
* "EPC QR" / GiroCode many European banking apps scan natively): the QR
|
|
13
|
+
* carries the exchange's IBAN, an optional amount, and the reserve public
|
|
14
|
+
* key as remittance text. Scanning it in a regular banking app produces a
|
|
15
|
+
* correctly addressed SEPA (Instant) Credit Transfer that funds the
|
|
16
|
+
* reserve, with no Taler-specific software on the payer's side.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Taler encodes binary data in Crockford base32 using the alphabet
|
|
21
|
+
* 0-9 A-Z without I, L, O, U. A reserve public key is a 32-byte EdDSA
|
|
22
|
+
* public key, which encodes to 52 characters.
|
|
23
|
+
*/
|
|
24
|
+
declare const RESERVE_PUB_RE: RegExp;
|
|
25
|
+
/** True when the input is a well-formed Taler reserve public key. */
|
|
26
|
+
declare function isValidReservePub(input: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the first well-formed reserve public key token from a transfer
|
|
29
|
+
* subject. Exchanges tolerate surrounding text in the subject line, and
|
|
30
|
+
* some banks prepend or append their own text; this scans token-wise.
|
|
31
|
+
*/
|
|
32
|
+
declare function findReservePub(subject: string): string | undefined;
|
|
33
|
+
interface TalerTopupIssue {
|
|
34
|
+
field: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
declare class TalerTopupError extends Error {
|
|
38
|
+
readonly issues: TalerTopupIssue[];
|
|
39
|
+
constructor(message: string, issues: TalerTopupIssue[]);
|
|
40
|
+
}
|
|
41
|
+
interface TalerTopupOptions {
|
|
42
|
+
/** Account holder name of the exchange's bank account, up to 70 characters. */
|
|
43
|
+
accountName: string;
|
|
44
|
+
/** IBAN of the exchange's bank account. */
|
|
45
|
+
iban: string;
|
|
46
|
+
/** BIC of the exchange's bank; optional within the EEA. */
|
|
47
|
+
bic?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Amount in euro (number or numeric string). Omit for an open-amount QR
|
|
50
|
+
* where the payer chooses the top-up amount in their banking app.
|
|
51
|
+
*/
|
|
52
|
+
amount?: number | string;
|
|
53
|
+
/** The Taler reserve public key to credit (52-character Crockford base32). */
|
|
54
|
+
reservePub: string;
|
|
55
|
+
/** Use CRLF as the payload element separator instead of LF. */
|
|
56
|
+
crlf?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds an EPC069-12 QR payload that tops up the given Taler reserve when
|
|
60
|
+
* scanned and confirmed in the payer's own banking app. Render it with any
|
|
61
|
+
* QR library at error correction level M.
|
|
62
|
+
*
|
|
63
|
+
* @throws TalerTopupError on a malformed reserve public key; EpcQrError on
|
|
64
|
+
* invalid bank data (propagated from @eupi/qr).
|
|
65
|
+
*/
|
|
66
|
+
declare function encodeTalerTopupQr(options: TalerTopupOptions): string;
|
|
67
|
+
interface TalerTopup {
|
|
68
|
+
/** The reserve public key found in the remittance text. */
|
|
69
|
+
reservePub: string;
|
|
70
|
+
/** IBAN of the receiving (exchange) account. */
|
|
71
|
+
iban: string;
|
|
72
|
+
/** Account holder name of the receiving account. */
|
|
73
|
+
accountName: string;
|
|
74
|
+
bic?: string;
|
|
75
|
+
/** Amount in euro as a numeric string, when the QR fixes one. */
|
|
76
|
+
amount?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parses an EPC069-12 payload and extracts the Taler top-up details.
|
|
80
|
+
*
|
|
81
|
+
* @throws EpcQrError when the payload is not a valid EPC QR;
|
|
82
|
+
* TalerTopupError when it contains no reserve public key.
|
|
83
|
+
*/
|
|
84
|
+
declare function parseTalerTopupQr(payload: string): TalerTopup;
|
|
85
|
+
|
|
86
|
+
export { RESERVE_PUB_RE, type TalerTopup, TalerTopupError, type TalerTopupIssue, type TalerTopupOptions, encodeTalerTopupQr, findReservePub, isValidReservePub, parseTalerTopupQr };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { EpcQrError, decodeEpcQr, encodeEpcQr } from "@eupi/qr";
|
|
3
|
+
var RESERVE_PUB_RE = /^[0-9A-HJKMNP-TV-Z]{52}$/;
|
|
4
|
+
function isValidReservePub(input) {
|
|
5
|
+
return RESERVE_PUB_RE.test(input);
|
|
6
|
+
}
|
|
7
|
+
function findReservePub(subject) {
|
|
8
|
+
for (const token of subject.toUpperCase().split(/[^0-9A-Z]+/)) {
|
|
9
|
+
if (RESERVE_PUB_RE.test(token)) return token;
|
|
10
|
+
}
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
var TalerTopupError = class extends Error {
|
|
14
|
+
issues;
|
|
15
|
+
constructor(message, issues) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TalerTopupError";
|
|
18
|
+
this.issues = issues;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function encodeTalerTopupQr(options) {
|
|
22
|
+
const reservePub = options.reservePub.toUpperCase();
|
|
23
|
+
if (!isValidReservePub(reservePub)) {
|
|
24
|
+
throw new TalerTopupError("invalid Taler reserve public key", [
|
|
25
|
+
{
|
|
26
|
+
field: "reservePub",
|
|
27
|
+
message: "must be 52 Crockford base32 characters (0-9, A-Z without I, L, O, U)"
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
return encodeEpcQr({
|
|
32
|
+
name: options.accountName,
|
|
33
|
+
iban: options.iban,
|
|
34
|
+
text: reservePub,
|
|
35
|
+
...options.bic !== void 0 ? { bic: options.bic } : {},
|
|
36
|
+
...options.amount !== void 0 ? { amount: options.amount } : {},
|
|
37
|
+
...options.crlf !== void 0 ? { crlf: options.crlf } : {}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function parseTalerTopupQr(payload) {
|
|
41
|
+
const { data } = decodeEpcQr(payload);
|
|
42
|
+
const subject = data.text ?? data.reference ?? "";
|
|
43
|
+
const reservePub = findReservePub(subject);
|
|
44
|
+
if (reservePub === void 0) {
|
|
45
|
+
throw new TalerTopupError("no Taler reserve public key in remittance information", [
|
|
46
|
+
{ field: "text", message: "expected a 52-character Crockford base32 reserve public key" }
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
reservePub,
|
|
51
|
+
iban: data.iban,
|
|
52
|
+
accountName: data.name,
|
|
53
|
+
...data.bic !== void 0 ? { bic: data.bic } : {},
|
|
54
|
+
...data.amount !== void 0 ? { amount: data.amount } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
EpcQrError,
|
|
59
|
+
RESERVE_PUB_RE,
|
|
60
|
+
TalerTopupError,
|
|
61
|
+
encodeTalerTopupQr,
|
|
62
|
+
findReservePub,
|
|
63
|
+
isValidReservePub,
|
|
64
|
+
parseTalerTopupQr
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Bridge between GNU Taler and the European payment QR standards.\n *\n * A Taler wallet is funded by wiring money to the exchange's bank account\n * with the reserve public key as the transfer subject: the exchange watches\n * its account (via LibEuFin/Nexus), finds the reserve public key in the\n * subject of an incoming credit, and credits that reserve.\n *\n * This package encodes that flow as a standard EPC069-12 QR code (the\n * \"EPC QR\" / GiroCode many European banking apps scan natively): the QR\n * carries the exchange's IBAN, an optional amount, and the reserve public\n * key as remittance text. Scanning it in a regular banking app produces a\n * correctly addressed SEPA (Instant) Credit Transfer that funds the\n * reserve, with no Taler-specific software on the payer's side.\n */\n\nimport { EpcQrError, decodeEpcQr, encodeEpcQr } from \"@eupi/qr\";\n\n/**\n * Taler encodes binary data in Crockford base32 using the alphabet\n * 0-9 A-Z without I, L, O, U. A reserve public key is a 32-byte EdDSA\n * public key, which encodes to 52 characters.\n */\nexport const RESERVE_PUB_RE = /^[0-9A-HJKMNP-TV-Z]{52}$/;\n\n/** True when the input is a well-formed Taler reserve public key. */\nexport function isValidReservePub(input: string): boolean {\n return RESERVE_PUB_RE.test(input);\n}\n\n/**\n * Extracts the first well-formed reserve public key token from a transfer\n * subject. Exchanges tolerate surrounding text in the subject line, and\n * some banks prepend or append their own text; this scans token-wise.\n */\nexport function findReservePub(subject: string): string | undefined {\n for (const token of subject.toUpperCase().split(/[^0-9A-Z]+/)) {\n if (RESERVE_PUB_RE.test(token)) return token;\n }\n return undefined;\n}\n\nexport interface TalerTopupIssue {\n field: string;\n message: string;\n}\n\nexport class TalerTopupError extends Error {\n readonly issues: TalerTopupIssue[];\n constructor(message: string, issues: TalerTopupIssue[]) {\n super(message);\n this.name = \"TalerTopupError\";\n this.issues = issues;\n }\n}\n\nexport interface TalerTopupOptions {\n /** Account holder name of the exchange's bank account, up to 70 characters. */\n accountName: string;\n /** IBAN of the exchange's bank account. */\n iban: string;\n /** BIC of the exchange's bank; optional within the EEA. */\n bic?: string;\n /**\n * Amount in euro (number or numeric string). Omit for an open-amount QR\n * where the payer chooses the top-up amount in their banking app.\n */\n amount?: number | string;\n /** The Taler reserve public key to credit (52-character Crockford base32). */\n reservePub: string;\n /** Use CRLF as the payload element separator instead of LF. */\n crlf?: boolean;\n}\n\n/**\n * Builds an EPC069-12 QR payload that tops up the given Taler reserve when\n * scanned and confirmed in the payer's own banking app. Render it with any\n * QR library at error correction level M.\n *\n * @throws TalerTopupError on a malformed reserve public key; EpcQrError on\n * invalid bank data (propagated from @eupi/qr).\n */\nexport function encodeTalerTopupQr(options: TalerTopupOptions): string {\n const reservePub = options.reservePub.toUpperCase();\n if (!isValidReservePub(reservePub)) {\n throw new TalerTopupError(\"invalid Taler reserve public key\", [\n {\n field: \"reservePub\",\n message: \"must be 52 Crockford base32 characters (0-9, A-Z without I, L, O, U)\",\n },\n ]);\n }\n return encodeEpcQr({\n name: options.accountName,\n iban: options.iban,\n text: reservePub,\n ...(options.bic !== undefined ? { bic: options.bic } : {}),\n ...(options.amount !== undefined ? { amount: options.amount } : {}),\n ...(options.crlf !== undefined ? { crlf: options.crlf } : {}),\n });\n}\n\nexport interface TalerTopup {\n /** The reserve public key found in the remittance text. */\n reservePub: string;\n /** IBAN of the receiving (exchange) account. */\n iban: string;\n /** Account holder name of the receiving account. */\n accountName: string;\n bic?: string;\n /** Amount in euro as a numeric string, when the QR fixes one. */\n amount?: string;\n}\n\n/**\n * Parses an EPC069-12 payload and extracts the Taler top-up details.\n *\n * @throws EpcQrError when the payload is not a valid EPC QR;\n * TalerTopupError when it contains no reserve public key.\n */\nexport function parseTalerTopupQr(payload: string): TalerTopup {\n const { data } = decodeEpcQr(payload);\n const subject = data.text ?? data.reference ?? \"\";\n const reservePub = findReservePub(subject);\n if (reservePub === undefined) {\n throw new TalerTopupError(\"no Taler reserve public key in remittance information\", [\n { field: \"text\", message: \"expected a 52-character Crockford base32 reserve public key\" },\n ]);\n }\n return {\n reservePub,\n iban: data.iban,\n accountName: data.name,\n ...(data.bic !== undefined ? { bic: data.bic } : {}),\n ...(data.amount !== undefined ? { amount: data.amount } : {}),\n };\n}\n\nexport { EpcQrError };\n"],"mappings":";AAgBA,SAAS,YAAY,aAAa,mBAAmB;AAO9C,IAAM,iBAAiB;AAGvB,SAAS,kBAAkB,OAAwB;AACxD,SAAO,eAAe,KAAK,KAAK;AAClC;AAOO,SAAS,eAAe,SAAqC;AAClE,aAAW,SAAS,QAAQ,YAAY,EAAE,MAAM,YAAY,GAAG;AAC7D,QAAI,eAAe,KAAK,KAAK,EAAG,QAAO;AAAA,EACzC;AACA,SAAO;AACT;AAOO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC;AAAA,EACT,YAAY,SAAiB,QAA2B;AACtD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AA4BO,SAAS,mBAAmB,SAAoC;AACrE,QAAM,aAAa,QAAQ,WAAW,YAAY;AAClD,MAAI,CAAC,kBAAkB,UAAU,GAAG;AAClC,UAAM,IAAI,gBAAgB,oCAAoC;AAAA,MAC5D;AAAA,QACE,OAAO;AAAA,QACP,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,YAAY;AAAA,IACjB,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,MAAM;AAAA,IACN,GAAI,QAAQ,QAAQ,SAAY,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IACxD,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,IACjE,GAAI,QAAQ,SAAS,SAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,EAC7D,CAAC;AACH;AAoBO,SAAS,kBAAkB,SAA6B;AAC7D,QAAM,EAAE,KAAK,IAAI,YAAY,OAAO;AACpC,QAAM,UAAU,KAAK,QAAQ,KAAK,aAAa;AAC/C,QAAM,aAAa,eAAe,OAAO;AACzC,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI,gBAAgB,yDAAyD;AAAA,MACjF,EAAE,OAAO,QAAQ,SAAS,8DAA8D;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,EAC7D;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eupi/taler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Top up GNU Taler reserves with standard European payment QR codes (EPC069-12 / EN 18184)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"taler",
|
|
7
|
+
"gnu-taler",
|
|
8
|
+
"sepa",
|
|
9
|
+
"sepa-instant",
|
|
10
|
+
"qr",
|
|
11
|
+
"girocode",
|
|
12
|
+
"epc-qr",
|
|
13
|
+
"payments",
|
|
14
|
+
"withdrawal",
|
|
15
|
+
"reserve"
|
|
16
|
+
],
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/LedgerInnovation/eupi.git",
|
|
21
|
+
"directory": "packages/taler"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/index.cjs",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@eupi/qr": "workspace:^"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"tsup": "^8.5.0",
|
|
55
|
+
"typescript": "^5.8.0",
|
|
56
|
+
"vitest": "^3.2.0"
|
|
57
|
+
}
|
|
58
|
+
}
|