taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 12f7657f6391f59c60dfc1824c31037beeb2b4ab
parent aa80c3d6bf1ea18c71e4d6a963e67e972f6e68a1
Author: Florian Dold <florian@dold.me>
Date:   Sun, 19 Jul 2026 00:26:02 +0200

util: add void payto, fix host port path parsing

Diffstat:
Mpackages/taler-util/src/payto.test.ts | 14++++++++++++--
Mpackages/taler-util/src/payto.ts | 36+++++++++++++++++++++++++++++++-----
Mpackages/taler-wallet-core/src/coinSelection.ts | 16++++++++++++----
3 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts @@ -20,8 +20,8 @@ import { test } from "node:test"; import { addPaytoQueryParams, HostPortPath, - PaytoType, Paytos, + PaytoType, } from "./payto.js"; import { Result } from "./result.js"; @@ -59,7 +59,10 @@ test("basic x-taler-bank payto string", (t) => { } assert.strictEqual(result.normalizedPath, "bank.demo.taler.net/accountName"); assert.strictEqual(result.host, "bank.demo.taler.net"); - assert.strictEqual(result.url, "https://bank.demo.taler.net/" as HostPortPath); + assert.strictEqual( + result.url, + "https://bank.demo.taler.net/" as HostPortPath, + ); assert.strictEqual(result.account, "accountName"); }); @@ -187,3 +190,10 @@ test("Paytos.parseHostPortPath keeps the path", () => { "https://example.com/foo/bar/", ); }); + +test("void payto URI", () => { + assert.ok(Result.isOk(Paytos.fromString("payto://void/foo"))); + assert.ok( + Result.isOk(Paytos.fromString("payto://void/foo?receiver-name=Bla")), + ); +}); diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -41,6 +41,7 @@ export enum PaytoType { TalerReserve = "taler-reserve", TalerReserveHttp = "taler-reserve-http", Ethereum = "ethereum", + Void = "void", } export enum ReservePubParseError { @@ -94,7 +95,8 @@ export namespace Paytos { | PaytoTalerReserveHttp | PaytoTalerBank | PaytoEthereum - | PaytoBitcoin; + | PaytoBitcoin + | PaytoVoid; declare const __full_payto_str: unique symbol; export type FullPaytoString = string & { [__full_payto_str]: true }; @@ -165,6 +167,10 @@ export namespace Paytos { account: string; } + export interface PaytoVoid extends PaytoGeneric { + targetType: PaytoType.Void; + } + export interface PaytoBitcoin extends PaytoGeneric { targetType: PaytoType.Bitcoin; address: BtAddrString; @@ -177,7 +183,7 @@ export namespace Paytos { address: EthAddrString; } - const supported_targets: Record<PaytoType, true> = { + const supportedTargets: Record<PaytoType, true> = { iban: true, bitcoin: true, "x-taler-bank": true, @@ -185,6 +191,7 @@ export namespace Paytos { "taler-reserve-http": true, ethereum: true, cyclos: true, + void: true, }; export function hashFull( @@ -316,8 +323,11 @@ export namespace Paytos { export function parseHostPortPath( hostnameAndPath: string, ): HostPortPath | undefined { - const [host, path] = hostnameAndPath.split("/", 1); - return parseHostPortPath2(host, path ?? ""); + const slashIdx = hostnameAndPath.indexOf("/"); + const host = + slashIdx < 0 ? hostnameAndPath : hostnameAndPath.slice(0, slashIdx); + const path = slashIdx < 0 ? "" : hostnameAndPath.slice(slashIdx + 1); + return parseHostPortPath2(host, path); } /** @@ -487,6 +497,19 @@ export namespace Paytos { }; } + export function createPaytoVoid( + voidPath: string, + params: Record<string, string> = {}, + ): PaytoVoid { + return { + targetType: PaytoType.Void, + params, + normalizedPath: `void/${voidPath}`, + fullPath: `void/${voidPath}`, + displayName: `void(${voidPath})`, + }; + } + ////////////////////// // parsing function /////////////////////// @@ -583,7 +606,7 @@ export namespace Paytos { const targetType = ( firstSlashPos === -1 ? acct : acct.slice(0, firstSlashPos) ) as PaytoType; - if (!opts.allowUnsupported && !supported_targets[targetType]) { + if (!opts.allowUnsupported && !supportedTargets[targetType]) { return Result.errorWithDetail(PaytoParseError.UNSUPPORTED, { targetType, }); @@ -803,6 +826,9 @@ export namespace Paytos { createCyclos(host ?? (cs[0] as HostPortPath), accountId, params), ); } + case PaytoType.Void: { + return Result.of<URI>(createPaytoVoid(targetPath, params)); + } default: { if (opts.allowUnsupported) { return Result.of<URI>( diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -47,10 +47,10 @@ import { InsufficientBalanceHint, j2s, Logger, - Paytos, - Result, PayCoinSelection, PaymentInsufficientBalanceDetails, + Paytos, + Result, ScopeInfo, ScopeType, SelectedCoin, @@ -202,7 +202,9 @@ async function internalSelectPayCoins( > { let restrictWireMethod; if (req.depositPaytoUri) { - const parsedPayto = Result.orUndefined(Paytos.fromString(req.depositPaytoUri)); + const parsedPayto = Result.orUndefined( + Paytos.fromString(req.depositPaytoUri), + ); if (!parsedPayto) { throw Error("invalid payto URI"); } @@ -846,7 +848,13 @@ export function findMatchingWire( | undefined { const accountRestrictions: Record<string, AccountRestriction[]> = {}; for (const acc of exchangeWireDetails.wireInfo.accounts) { - const pp = Result.orUndefined(Paytos.fromString(acc.payto_uri)); + const ppRes = Paytos.fromString(acc.payto_uri); + if (Result.isError(ppRes)) { + throw Error( + `failed to parse payto ${acc.payto_uri}`, + ); + } + const pp = ppRes.value; checkLogicInvariant(!!pp); if (pp.targetType !== wireMethod) { continue;