aboutsummaryrefslogtreecommitdiff
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
parentfb8372dfbf27b7b4e8b2fe4f81aa2ba18bfcf638 (diff)
implement peer to peer push payments
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx11
-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
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts86
-rw-r--r--packages/taler-wallet-cli/src/harness/libeufin.ts43
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts3
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts9
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts8
-rw-r--r--packages/taler-wallet-core/src/db.ts168
-rw-r--r--packages/taler-wallet-core/src/index.ts1
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts24
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts185
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts13
-rw-r--r--packages/taler-wallet-core/src/operations/peer-to-peer.ts149
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts40
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts65
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts843
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts15
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts88
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts657
-rw-r--r--packages/taler-wallet-core/src/pending-types.ts18
-rw-r--r--packages/taler-wallet-core/src/wallet.ts124
28 files changed, 1095 insertions, 1632 deletions
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 7cc65a62d..f83131ae1 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -28,15 +28,14 @@ interface Props {
mobile?: boolean;
}
-const VERSION: string = process.env.__VERSION__ || "dev";
-const GIT_HASH: string | undefined = process.env.__GIT_HASH__;
+// @ts-ignore
+const maybeEnv = process?.env || {};
+
+const VERSION: string = maybeEnv.__VERSION__ || "dev";
+const GIT_HASH: string | undefined = maybeEnv.__GIT_HASH__;
const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
export function Sidebar({ mobile }: Props): VNode {
- // const config = useConfigContext();
- const config = { version: "none" };
- // FIXME: add replacement for __VERSION__ with the current version
- const process = { env: { __VERSION__: "0.0.0" } };
const reducer = useAnastasisContext()!;
function saveSession(): void {
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");
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts
index 3b58219bb..c735c9956 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -70,7 +70,7 @@ import {
TipCreateConfirmation,
TipCreateRequest,
TippingReserveStatus,
-} from "./merchantApiTypes";
+} from "./merchantApiTypes.js";
const exec = util.promisify(require("child_process").exec);
@@ -478,14 +478,14 @@ class BankServiceBase {
protected globalTestState: GlobalTestState,
protected bankConfig: BankConfig,
protected configFile: string,
- ) { }
+ ) {}
}
/**
* Work in progress. The key point is that both Sandbox and Nexus
* will be configured and started by this class.
*/
-class EufinBankService extends BankServiceBase implements BankServiceHandle {
+class LibEuFinBankService extends BankServiceBase implements BankServiceHandle {
sandboxProc: ProcessWrapper | undefined;
nexusProc: ProcessWrapper | undefined;
@@ -494,8 +494,8 @@ class EufinBankService extends BankServiceBase implements BankServiceHandle {
static async create(
gc: GlobalTestState,
bc: BankConfig,
- ): Promise<EufinBankService> {
- return new EufinBankService(gc, bc, "foo");
+ ): Promise<LibEuFinBankService> {
+ return new LibEuFinBankService(gc, bc, "foo");
}
get port() {
@@ -761,7 +761,10 @@ class EufinBankService extends BankServiceBase implements BankServiceHandle {
}
}
-class PybankService extends BankServiceBase implements BankServiceHandle {
+/**
+ * Implementation of the bank service using the "taler-fakebank-run" tool.
+ */
+class FakebankService extends BankServiceBase implements BankServiceHandle {
proc: ProcessWrapper | undefined;
http = new NodeHttpLib();
@@ -769,41 +772,23 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
static async create(
gc: GlobalTestState,
bc: BankConfig,
- ): Promise<PybankService> {
+ ): Promise<FakebankService> {
const config = new Configuration();
setTalerPaths(config, gc.testDir + "/talerhome");
config.setString("taler", "currency", bc.currency);
- config.setString("bank", "database", bc.database);
config.setString("bank", "http_port", `${bc.httpPort}`);
config.setString("bank", "serve", "http");
config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
- config.setString(
- "bank",
- "allow_registrations",
- bc.allowRegistrations ? "yes" : "no",
- );
const cfgFilename = gc.testDir + "/bank.conf";
config.write(cfgFilename);
- await sh(
- gc,
- "taler-bank-manage_django",
- `taler-bank-manage -c '${cfgFilename}' django migrate`,
- );
- await sh(
- gc,
- "taler-bank-manage_django",
- `taler-bank-manage -c '${cfgFilename}' django provide_accounts`,
- );
-
- return new PybankService(gc, bc, cfgFilename);
+ return new FakebankService(gc, bc, cfgFilename);
}
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl);
- config.setString("bank", "suggested_exchange_payto", exchangePayto);
config.write(this.configFile);
}
@@ -815,21 +800,6 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
accountName: string,
password: string,
): Promise<HarnessExchangeBankAccount> {
- await sh(
- this.globalTestState,
- "taler-bank-manage_django",
- `taler-bank-manage -c '${this.configFile}' django add_bank_account ${accountName}`,
- );
- await sh(
- this.globalTestState,
- "taler-bank-manage_django",
- `taler-bank-manage -c '${this.configFile}' django changepassword_unsafe ${accountName} ${password}`,
- );
- await sh(
- this.globalTestState,
- "taler-bank-manage_django",
- `taler-bank-manage -c '${this.configFile}' django top_up ${accountName} ${this.bankConfig.currency}:100000`,
- );
return {
accountName: accountName,
accountPassword: password,
@@ -844,8 +814,8 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
async start(): Promise<void> {
this.proc = this.globalTestState.spawnService(
- "taler-bank-manage",
- ["-c", this.configFile, "serve"],
+ "taler-fakebank-run",
+ ["-c", this.configFile],
"bank",
);
}
@@ -857,7 +827,7 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
}
// Use libeufin bank instead of pybank.
-const useLibeufinBank = process.env.WALLET_HARNESS_WITH_EUFIN;
+const useLibeufinBank = true;
/**
* Return a euFin or a pyBank implementation of
@@ -866,21 +836,21 @@ const useLibeufinBank = process.env.WALLET_HARNESS_WITH_EUFIN;
* on a particular env variable.
*/
function getBankServiceImpl(): {
- prototype: typeof PybankService.prototype;
- create: typeof PybankService.create;
+ prototype: typeof FakebankService.prototype;
+ create: typeof FakebankService.create;
} {
if (useLibeufinBank)
return {
- prototype: EufinBankService.prototype,
- create: EufinBankService.create,
+ prototype: LibEuFinBankService.prototype,
+ create: LibEuFinBankService.create,
};
return {
- prototype: PybankService.prototype,
- create: PybankService.create,
+ prototype: FakebankService.prototype,
+ create: FakebankService.create,
};
}
-export type BankService = PybankService;
+export type BankService = FakebankService;
export const BankService = getBankServiceImpl();
export class FakeBankService {
@@ -923,7 +893,7 @@ export class FakeBankService {
private globalTestState: GlobalTestState,
private bankConfig: FakeBankConfig,
private configFile: string,
- ) { }
+ ) {}
async start(): Promise<void> {
this.proc = this.globalTestState.spawnService(
@@ -1189,7 +1159,7 @@ export class ExchangeService implements ExchangeServiceInterface {
private exchangeConfig: ExchangeConfig,
private configFilename: string,
private keyPair: EddsaKeyPair,
- ) { }
+ ) {}
get name() {
return this.exchangeConfig.name;
@@ -1442,7 +1412,7 @@ export class MerchantApiClient {
constructor(
private baseUrl: string,
public readonly auth: MerchantAuthConfiguration,
- ) { }
+ ) {}
async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
const url = new URL("private/auth", this.baseUrl);
@@ -1635,7 +1605,7 @@ export class MerchantService implements MerchantServiceInterface {
private globalState: GlobalTestState,
private merchantConfig: MerchantConfig,
private configFilename: string,
- ) { }
+ ) {}
private currentTimetravel: Duration | undefined;
@@ -1947,8 +1917,10 @@ export class WalletCli {
const resp = await sh(
self.globalTestState,
`wallet-${self.name}`,
- `taler-wallet-cli ${self.timetravelArg ?? ""
- } --no-throttle -LTRACE --wallet-db '${self.dbfile
+ `taler-wallet-cli ${
+ self.timetravelArg ?? ""
+ } --no-throttle -LTRACE --wallet-db '${
+ self.dbfile
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
);
console.log("--- wallet core response ---");
diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts
index 0107d5a8b..7356a6273 100644
--- a/packages/taler-wallet-cli/src/harness/libeufin.ts
+++ b/packages/taler-wallet-cli/src/harness/libeufin.ts
@@ -36,7 +36,7 @@ import {
runCommand,
setupDb,
sh,
- getRandomIban
+ getRandomIban,
} from "../harness/harness.js";
import {
LibeufinSandboxApi,
@@ -53,13 +53,10 @@ import {
CreateAnastasisFacadeRequest,
PostNexusTaskRequest,
PostNexusPermissionRequest,
- CreateNexusUserRequest
+ CreateNexusUserRequest,
} from "../harness/libeufin-apis.js";
-export {
- LibeufinSandboxApi,
- LibeufinNexusApi
-}
+export { LibeufinSandboxApi, LibeufinNexusApi };
export interface LibeufinServices {
libeufinSandbox: LibeufinSandboxService;
@@ -206,6 +203,16 @@ export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
}
async start(): Promise<void> {
+ await sh(
+ this.globalTestState,
+ "libeufin-sandbox-config",
+ "libeufin-sandbox config default",
+ {
+ ...process.env,
+ LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
+ },
+ );
+
this.sandboxProc = this.globalTestState.spawnService(
"libeufin-sandbox",
["serve", "--port", `${this.sandboxConfig.httpPort}`],
@@ -235,7 +242,8 @@ export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
debit: string,
credit: string,
amount: string, // $currency:x.y
- subject: string,): Promise<string> {
+ subject: string,
+ ): Promise<string> {
const stdout = await sh(
this.globalTestState,
"libeufin-sandbox-maketransfer",
@@ -428,7 +436,7 @@ export class LibeufinCli {
LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl,
LIBEUFIN_SANDBOX_USERNAME: "admin",
LIBEUFIN_SANDBOX_PASSWORD: "secret",
- }
+ };
}
async checkSandbox(): Promise<void> {
@@ -436,7 +444,7 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-checksandbox",
"libeufin-cli sandbox check",
- this.env()
+ this.env(),
);
}
@@ -445,7 +453,7 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createebicshost",
`libeufin-cli sandbox ebicshost create --host-id=${hostId}`,
- this.env()
+ this.env(),
);
console.log(stdout);
}
@@ -460,7 +468,7 @@ export class LibeufinCli {
` --host-id=${details.hostId}` +
` --partner-id=${details.partnerId}` +
` --user-id=${details.userId}`,
- this.env()
+ this.env(),
);
console.log(stdout);
}
@@ -480,7 +488,7 @@ export class LibeufinCli {
` --ebics-host-id=${sd.hostId}` +
` --ebics-partner-id=${sd.partnerId}` +
` --ebics-user-id=${sd.userId}`,
- this.env()
+ this.env(),
);
console.log(stdout);
}
@@ -490,7 +498,7 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-generatetransactions",
`libeufin-cli sandbox bankaccount generate-transactions ${accountName}`,
- this.env()
+ this.env(),
);
console.log(stdout);
}
@@ -500,7 +508,7 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-showsandboxtransactions",
`libeufin-cli sandbox bankaccount transactions ${accountName}`,
- this.env()
+ this.env(),
);
console.log(stdout);
}
@@ -834,9 +842,12 @@ export async function launchLibeufinServices(
libeufinNexus,
nb.twgHistoryPermission,
);
- break;
+ break;
case "anastasis":
- await LibeufinNexusApi.createAnastasisFacade(libeufinNexus, nb.anastasisReq);
+ await LibeufinNexusApi.createAnastasisFacade(
+ libeufinNexus,
+ nb.anastasisReq,
+ );
}
}
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts
index 84b401119..cb57c7d0a 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts
@@ -96,7 +96,7 @@ export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
debtorName: "mock2",
amount: "1",
subject: "mock subject",
- }
+ },
);
await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
let transactions = await LibeufinNexusApi.getAccountTransactions(
@@ -106,4 +106,5 @@ export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
let el = findNexusPayment("mock subject", transactions.data);
t.assertTrue(el instanceof Object);
}
+
runLibeufinApiBankaccountTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts
index aa5d4c9c0..ca7dc33d8 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts
@@ -17,12 +17,7 @@
/**
* Imports.
*/
-import {
- AbsoluteTime,
- ContractTerms,
- Duration,
- durationFromSpec,
-} from "@gnu-taler/taler-util";
+import { AbsoluteTime, ContractTerms, Duration } from "@gnu-taler/taler-util";
import {
WalletApiOperation,
HarnessExchangeBankAccount,
@@ -42,7 +37,7 @@ import {
LibeufinNexusService,
LibeufinSandboxApi,
LibeufinSandboxService,
-} from "../harness/libeufin";
+} from "../harness/libeufin.js";
const exchangeIban = "DE71500105179674997361";
const customerIban = "DE84500105176881385584";
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
index 5c716dc54..c22258bc8 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer.ts
@@ -22,7 +22,6 @@ import { GlobalTestState } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
- makeTestPayment,
} from "../harness/helpers.js";
/**
@@ -55,9 +54,7 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
const checkResp = await wallet.client.call(
WalletApiOperation.CheckPeerPushPayment,
{
- contractPriv: resp.contractPriv,
- exchangeBaseUrl: resp.exchangeBaseUrl,
- pursePub: resp.pursePub,
+ talerUri: resp.talerUri,
},
);
@@ -66,8 +63,7 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
const acceptResp = await wallet.client.call(
WalletApiOperation.AcceptPeerPushPayment,
{
- exchangeBaseUrl: resp.exchangeBaseUrl,
- pursePub: resp.pursePub,
+ peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
},
);
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index f763aae6b..8f558abd3 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -127,36 +127,6 @@ export interface ReserveBankInfo {
* Exchange payto URI that the bank will use to fund the reserve.
*/
exchangePaytoUri: string;
-}
-
-/**
- * A reserve record as stored in the wallet's database.
- */
-export interface ReserveRecord {
- /**
- * The reserve public key.
- */
- reservePub: string;
-
- /**
- * The reserve private key.
- */
- reservePriv: string;
-
- /**
- * The exchange base URL for the reserve.
- */
- exchangeBaseUrl: string;
-
- /**
- * Currency of the reserve.
- */
- currency: string;
-
- /**
- * Time when the reserve was created.
- */
- timestampCreated: TalerProtocolTimestamp;
/**
* Time when the information about this reserve was posted to the bank.
@@ -165,83 +135,14 @@ export interface ReserveRecord {
*
* Set to undefined if that hasn't happened yet.
*/
- timestampReserveInfoPosted: TalerProtocolTimestamp | undefined;
+ timestampReserveInfoPosted?: TalerProtocolTimestamp;
/**
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
- timestampBankConfirmed: TalerProtocolTimestamp | undefined;
-
- /**
- * Wire information (as payto URI) for the bank account that
- * transferred funds for this reserve.
- */
- senderWire?: string;
-
- /**
- * Amount that was sent by the user to fund the reserve.
- */
- instructedAmount: AmountJson;
-
- /**
- * Extra state for when this is a withdrawal involving
- * a Taler-integrated bank.
- */
- bankInfo?: ReserveBankInfo;
-
- /**
- * Restrict withdrawals from this reserve to this age.
- */
- restrictAge?: number;
-
- /**
- * Pre-allocated ID of the withdrawal group for the first withdrawal
- * on this reserve.
- */
- initialWithdrawalGroupId: string;
-
- /**
- * Did we start the first withdrawal for this reserve?
- *
- * We only report a pending withdrawal for the reserve before
- * the first withdrawal has started.
- */
- initialWithdrawalStarted: boolean;
-
- /**
- * Initial denomination selection, stored here so that
- * we can show this information in the transactions/balances
- * before we have a withdrawal group.
- */
- initialDenomSel: DenomSelectionState;
-
- /**
- * Current status of the reserve.
- */
- reserveStatus: ReserveRecordStatus;
-
- /**
- * Is there any work to be done for this reserve?
- *
- * Technically redundant, since the reserveStatus would indicate this.
- * However, we use the operationStatus for DB indexing of pending operations.
- */
- operationStatus: OperationStatus;
-
- /**
- * Retry info, in case the reserve needs to be processed again
- * later, either due to an error or because the wallet needs to
- * wait for something.
- */
- retryInfo: RetryInfo | undefined;
-
- /**
- * Last error that happened in a reserve operation
- * (either talking to the bank or the exchange).
- */
- lastError: TalerErrorDetail | undefined;
+ timestampBankConfirmed?: TalerProtocolTimestamp;
}
/**
@@ -514,6 +415,11 @@ export interface ExchangeDetailsPointer {
updateClock: TalerProtocolTimestamp;
}
+export interface MergeReserveInfo {
+ reservePub: string;
+ reservePriv: string;
+}
+
/**
* Exchange record as stored in the wallet's database.
*/
@@ -568,7 +474,7 @@ export interface ExchangeRecord {
* Public key of the reserve that we're currently using for
* receiving P2P payments.
*/
- currentMergeReservePub?: string;
+ currentMergeReserveInfo?: MergeReserveInfo;
}
/**
@@ -1373,6 +1279,7 @@ export interface WithdrawalGroupRecord {
/**
* Secret seed used to derive planchets.
+ * Stored since planchets are created lazily.
*/
secretSeed: string;
@@ -1382,6 +1289,11 @@ export interface WithdrawalGroupRecord {
reservePub: string;
/**
+ * The reserve private key.
+ */
+ reservePriv: string;
+
+ /**
* The exchange base URL that we're withdrawing from.
* (Redundantly stored, as the reserve record also has this info.)
*/
@@ -1395,8 +1307,6 @@ export interface WithdrawalGroupRecord {
/**
* When was the withdrawal operation completed?
- *
- * FIXME: We should probably drop this and introduce an OperationStatus field.
*/
timestampFinish?: TalerProtocolTimestamp;
@@ -1407,6 +1317,33 @@ export interface WithdrawalGroupRecord {
operationStatus: OperationStatus;
/**
+ * Current status of the reserve.
+ */
+ reserveStatus: ReserveRecordStatus;
+
+ /**
+ * Amount that was sent by the user to fund the reserve.
+ */
+ instructedAmount: AmountJson;
+
+ /**
+ * Wire information (as payto URI) for the bank account that
+ * transferred funds for this reserve.
+ */
+ senderWire?: string;
+
+ /**
+ * Restrict withdrawals from this reserve to this age.
+ */
+ restrictAge?: number;
+
+ /**
+ * Extra state for when this is a withdrawal involving
+ * a Taler-integrated bank.
+ */
+ bankInfo?: ReserveBankInfo;
+
+ /**
* Amount including fees (i.e. the amount subtracted from the
* reserve to withdraw all coins in this withdrawal session).
*/
@@ -1730,9 +1667,11 @@ export interface PeerPushPaymentInitiationRecord {
/**
* Record for a push P2P payment that this wallet was offered.
*
- * Primary key: (exchangeBaseUrl, pursePub)
+ * Unique: (exchangeBaseUrl, pursePub)
*/
export interface PeerPushPaymentIncomingRecord {
+ peerPushPaymentIncomingId: string;
+
exchangeBaseUrl: string;
pursePub: string;
@@ -1828,16 +1767,6 @@ export const WalletStoresV1 = {
}),
{},
),
- reserves: describeStore(
- describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
- {
- byInitialWithdrawalGroupId: describeIndex(
- "byInitialWithdrawalGroupId",
- "initialWithdrawalGroupId",
- ),
- byStatus: describeIndex("byStatus", "operationStatus"),
- },
- ),
purchases: describeStore(
describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
{
@@ -1926,9 +1855,14 @@ export const WalletStoresV1 = {
),
peerPushPaymentIncoming: describeStore(
describeContents<PeerPushPaymentIncomingRecord>("peerPushPaymentIncoming", {
- keyPath: ["exchangeBaseUrl", "pursePub"],
+ keyPath: "peerPushPaymentIncomingId",
}),
- {},
+ {
+ byExchangeAndPurse: describeIndex("byExchangeAndPurse", [
+ "exchangeBaseUrl",
+ "pursePub",
+ ]),
+ },
),
};
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 8b0f1749d..92fe852ac 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -53,7 +53,6 @@ export * from "./operations/exchanges.js";
export * from "./bank-api-client.js";
-export * from "./operations/reserves.js";
export * from "./operations/withdraw.js";
export * from "./operations/refresh.js";
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 7074128b0..0650ed040 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -73,15 +73,6 @@ export interface MerchantOperations {
): Promise<MerchantInfo>;
}
-export interface ReserveOperations {
- processReserve(
- ws: InternalWalletState,
- reservePub: string,
- options?: {
- forceNow?: boolean;
- },
- ): Promise<void>;
-}
/**
* Interface for exchange-related operations.
@@ -234,7 +225,6 @@ export interface InternalWalletState {
exchangeOps: ExchangeOperations;
recoupOps: RecoupOperations;
merchantOps: MerchantOperations;
- reserveOps: ReserveOperations;
getDenomInfo(
ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index d4c822972..c77ce1a85 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -88,7 +88,6 @@ export async function exportBackup(
backupProviders: x.backupProviders,
tips: x.tips,
recoupGroups: x.recoupGroups,
- reserves: x.reserves,
withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
@@ -128,29 +127,6 @@ export async function exportBackup(
});
});
- await tx.reserves.iter().forEach((reserve) => {
- const backupReserve: BackupReserve = {
- initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
- (x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- }),
- ),
- initial_withdrawal_group_id: reserve.initialWithdrawalGroupId,
- instructed_amount: Amounts.stringify(reserve.instructedAmount),
- reserve_priv: reserve.reservePriv,
- timestamp_created: reserve.timestampCreated,
- withdrawal_groups:
- withdrawalGroupsByReserve[reserve.reservePub] ?? [],
- // FIXME!
- timestamp_last_activity: reserve.timestampCreated,
- };
- const backupReserves = (backupReservesByExchange[
- reserve.exchangeBaseUrl
- ] ??= []);
- backupReserves.push(backupReserve);
- });
-
await tx.tips.iter().forEach((tip) => {
backupTips.push({
exchange_base_url: tip.exchangeBaseUrl,
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index e099fae57..f26c42770 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -236,7 +236,6 @@ export async function importBackup(
backupProviders: x.backupProviders,
tips: x.tips,
recoupGroups: x.recoupGroups,
- reserves: x.reserves,
withdrawalGroups: x.withdrawalGroups,
tombstones: x.tombstones,
depositGroups: x.depositGroups,
@@ -427,94 +426,98 @@ export async function importBackup(
}
}
- for (const backupReserve of backupExchangeDetails.reserves) {
- const reservePub =
- cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
- const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
- if (tombstoneSet.has(ts)) {
- continue;
- }
- checkLogicInvariant(!!reservePub);
- const existingReserve = await tx.reserves.get(reservePub);
- const instructedAmount = Amounts.parseOrThrow(
- backupReserve.instructed_amount,
- );
- if (!existingReserve) {
- let bankInfo: ReserveBankInfo | undefined;
- if (backupReserve.bank_info) {
- bankInfo = {
- exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
- statusUrl: backupReserve.bank_info.status_url,
- confirmUrl: backupReserve.bank_info.confirm_url,
- };
- }
- await tx.reserves.put({
- currency: instructedAmount.currency,
- instructedAmount,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- reservePub,
- reservePriv: backupReserve.reserve_priv,
- bankInfo,
- timestampCreated: backupReserve.timestamp_created,
- timestampBankConfirmed:
- backupReserve.bank_info?.timestamp_bank_confirmed,
- timestampReserveInfoPosted:
- backupReserve.bank_info?.timestamp_reserve_info_posted,
- senderWire: backupReserve.sender_wire,
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- initialWithdrawalGroupId:
- backupReserve.initial_withdrawal_group_id,
- initialWithdrawalStarted:
- backupReserve.withdrawal_groups.length > 0,
- // FIXME!
- reserveStatus: ReserveRecordStatus.QueryingStatus,
- initialDenomSel: await getDenomSelStateFromBackup(
- tx,
- backupExchangeDetails.base_url,
- backupReserve.initial_selected_denoms,
- ),
- // FIXME!
- operationStatus: OperationStatus.Pending,
- });
- }
- for (const backupWg of backupReserve.withdrawal_groups) {
- const ts = makeEventId(
- TombstoneTag.DeleteWithdrawalGroup,
- backupWg.withdrawal_group_id,
- );
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingWg = await tx.withdrawalGroups.get(
- backupWg.withdrawal_group_id,
- );
- if (!existingWg) {
- await tx.withdrawalGroups.put({
- denomsSel: await getDenomSelStateFromBackup(
- tx,
- backupExchangeDetails.base_url,
- backupWg.selected_denoms,
- ),
- exchangeBaseUrl: backupExchangeDetails.base_url,
- lastError: undefined,
- rawWithdrawalAmount: Amounts.parseOrThrow(
- backupWg.raw_withdrawal_amount,
- ),
- reservePub,
- retryInfo: RetryInfo.reset(),
- secretSeed: backupWg.secret_seed,
- timestampStart: backupWg.timestamp_created,
- timestampFinish: backupWg.timestamp_finish,
- withdrawalGroupId: backupWg.withdrawal_group_id,
- denomSelUid: backupWg.selected_denoms_id,
- operationStatus: backupWg.timestamp_finish
- ? OperationStatus.Finished
- : OperationStatus.Pending,
- });
- }
- }
- }
+
+ // FIXME: import reserves with new schema
+
+ // for (const backupReserve of backupExchangeDetails.reserves) {
+ // const reservePub =
+ // cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
+ // const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
+ // if (tombstoneSet.has(ts)) {
+ // continue;
+ // }
+ // checkLogicInvariant(!!reservePub);
+ // const existingReserve = await tx.reserves.get(reservePub);
+ // const instructedAmount = Amounts.parseOrThrow(
+ // backupReserve.instructed_amount,
+ // );
+ // if (!existingReserve) {
+ // let bankInfo: ReserveBankInfo | undefined;
+ // if (backupReserve.bank_info) {
+ // bankInfo = {
+ // exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
+ // statusUrl: backupReserve.bank_info.status_url,
+ // confirmUrl: backupReserve.bank_info.confirm_url,
+ // };
+ // }
+ // await tx.reserves.put({
+ // currency: instructedAmount.currency,
+ // instructedAmount,
+ // exchangeBaseUrl: backupExchangeDetails.base_url,
+ // reservePub,
+ // reservePriv: backupReserve.reserve_priv,
+ // bankInfo,
+ // timestampCreated: backupReserve.timestamp_created,
+ // timestampBankConfirmed:
+ // backupReserve.bank_info?.timestamp_bank_confirmed,
+ // timestampReserveInfoPosted:
+ // backupReserve.bank_info?.timestamp_reserve_info_posted,
+ // senderWire: backupReserve.sender_wire,
+ // retryInfo: RetryInfo.reset(),
+ // lastError: undefined,
+ // initialWithdrawalGroupId:
+ // backupReserve.initial_withdrawal_group_id,
+ // initialWithdrawalStarted:
+ // backupReserve.withdrawal_groups.length > 0,
+ // // FIXME!
+ // reserveStatus: ReserveRecordStatus.QueryingStatus,
+ // initialDenomSel: await getDenomSelStateFromBackup(
+ // tx,
+ // backupExchangeDetails.base_url,
+ // backupReserve.initial_selected_denoms,
+ // ),
+ // // FIXME!
+ // operationStatus: OperationStatus.Pending,
+ // });
+ // }
+ // for (const backupWg of backupReserve.withdrawal_groups) {
+ // const ts = makeEventId(
+ // TombstoneTag.DeleteWithdrawalGroup,
+ // backupWg.withdrawal_group_id,
+ // );
+ // if (tombstoneSet.has(ts)) {
+ // continue;
+ // }
+ // const existingWg = await tx.withdrawalGroups.get(
+ // backupWg.withdrawal_group_id,
+ // );
+ // if (!existingWg) {
+ // await tx.withdrawalGroups.put({
+ // denomsSel: await getDenomSelStateFromBackup(
+ // tx,
+ // backupExchangeDetails.base_url,
+ // backupWg.selected_denoms,
+ // ),
+ // exchangeBaseUrl: backupExchangeDetails.base_url,
+ // lastError: undefined,
+ // rawWithdrawalAmount: Amounts.parseOrThrow(
+ // backupWg.raw_withdrawal_amount,
+ // ),
+ // reservePub,
+ // retryInfo: RetryInfo.reset(),
+ // secretSeed: backupWg.secret_seed,
+ // timestampStart: backupWg.timestamp_created,
+ // timestampFinish: backupWg.timestamp_finish,
+ // withdrawalGroupId: backupWg.withdrawal_group_id,
+ // denomSelUid: backupWg.selected_denoms_id,
+ // operationStatus: backupWg.timestamp_finish
+ // ? OperationStatus.Finished
+ // : OperationStatus.Pending,
+ // });
+ // }
+ // }
+ // }
+
}
for (const backupProposal of backupBlob.proposals) {
@@ -920,10 +923,6 @@ export async function importBackup(
} else if (type === TombstoneTag.DeleteRefund) {
// Nothing required, will just prevent display
// in the transactions list
- } else if (type === TombstoneTag.DeleteReserve) {
- // FIXME: Once we also have account (=kyc) reserves,
- // we need to check if the reserve is an account before deleting here
- await tx.reserves.delete(rest[0]);
} else if (type === TombstoneTag.DeleteTip) {
await tx.tips.delete(rest[0]);
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index c26eb0cfc..4590f5051 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -41,7 +41,6 @@ interface WalletBalance {
export async function getBalancesInsideTransaction(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
- reserves: typeof WalletStoresV1.reserves;
coins: typeof WalletStoresV1.coins;
refreshGroups: typeof WalletStoresV1.refreshGroups;
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
@@ -65,17 +64,6 @@ export async function getBalancesInsideTransaction(
return balanceStore[currency];
};
- // Initialize balance to zero, even if we didn't start withdrawing yet.
- await tx.reserves.iter().forEach((r) => {
- const b = initBalance(r.currency);
- if (!r.initialWithdrawalStarted) {
- b.pendingIncoming = Amounts.add(
- b.pendingIncoming,
- r.initialDenomSel.totalCoinValue,
- ).amount;
- }
- });
-
await tx.coins.iter().forEach((c) => {
// Only count fresh coins, as dormant coins will
// already be in a refresh session.
@@ -154,7 +142,6 @@ export async function getBalances(
.mktx((x) => ({
coins: x.coins,
refreshGroups: x.refreshGroups,
- reserves: x.reserves,
purchases: x.purchases,
withdrawalGroups: x.withdrawalGroups,
}))
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index 658cbe4f7..4d2f2bb5f 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2022 GNUnet e.V.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -30,35 +30,35 @@ import {
codecForAmountString,
codecForAny,
codecForExchangeGetContractResponse,
+ constructPayPushUri,
ContractTermsUtil,
decodeCrock,
Duration,
eddsaGetPublic,
encodeCrock,
ExchangePurseMergeRequest,
+ getRandomBytes,
InitiatePeerPushPaymentRequest,
InitiatePeerPushPaymentResponse,
j2s,
Logger,
+ parsePayPushUri,
strcmp,
TalerProtocolTimestamp,
UnblindedSignature,
WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
-import { url } from "inspector";
import {
CoinStatus,
+ MergeReserveInfo,
OperationStatus,
- ReserveRecord,
ReserveRecordStatus,
+ WithdrawalGroupRecord,
} from "../db.js";
-import {
- checkSuccessResponseOrThrow,
- readSuccessResponseJsonOrThrow,
- throwUnexpectedRequestError,
-} from "../util/http.js";
+import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
+import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
@@ -265,6 +265,10 @@ export async function initiatePeerToPeerPush(
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ talerUri: constructPayPushUri({
+ exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
+ contractPriv: econtractResp.contractPriv,
+ }),
};
}
@@ -281,26 +285,19 @@ export async function checkPeerPushPayment(
ws: InternalWalletState,
req: CheckPeerPushPaymentRequest,
): Promise<CheckPeerPushPaymentResponse> {
- const getPurseUrl = new URL(
- `purses/${req.pursePub}/deposit`,
- req.exchangeBaseUrl,
- );
+ // FIXME: Check if existing record exists!
- const contractPub = encodeCrock(
- eddsaGetPublic(decodeCrock(req.contractPriv)),
- );
+ const uri = parsePayPushUri(req.talerUri);
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
+ if (!uri) {
+ throw Error("got invalid taler://pay-push URI");
+ }
- const purseStatus = await readSuccessResponseJsonOrThrow(
- purseHttpResp,
- codecForExchangePurseStatus(),
- );
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
- const getContractUrl = new URL(
- `contracts/${contractPub}`,
- req.exchangeBaseUrl,
- );
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
const contractHttpResp = await ws.http.get(getContractUrl.href);
@@ -309,22 +306,36 @@ export async function checkPeerPushPayment(
codecForExchangeGetContractResponse(),
);
+ const pursePub = contractResp.purse_pub;
+
const dec = await ws.cryptoApi.decryptContractForMerge({
ciphertext: contractResp.econtract,
- contractPriv: req.contractPriv,
- pursePub: req.pursePub,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
});
+ const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
+
+ const purseHttpResp = await ws.http.get(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
+
await ws.db
.mktx((x) => ({
peerPushPaymentIncoming: x.peerPushPaymentIncoming,
}))
.runReadWrite(async (tx) => {
await tx.peerPushPaymentIncoming.add({
- contractPriv: req.contractPriv,
- exchangeBaseUrl: req.exchangeBaseUrl,
+ peerPushPaymentIncomingId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
mergePriv: dec.mergePriv,
- pursePub: req.pursePub,
+ pursePub: pursePub,
timestampAccepted: TalerProtocolTimestamp.now(),
contractTerms: dec.contractTerms,
});
@@ -333,6 +344,7 @@ export async function checkPeerPushPayment(
return {
amount: purseStatus.balance,
contractTerms: dec.contractTerms,
+ peerPushPaymentIncomingId,
};
}
@@ -343,9 +355,9 @@ export function talerPaytoFromExchangeReserve(
const url = new URL(exchangeBaseUrl);
let proto: string;
if (url.protocol === "http:") {
- proto = "taler+http";
+ proto = "taler-reserve-http";
} else if (url.protocol === "https:") {
- proto = "taler";
+ proto = "taler-reserve";
} else {
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
}
@@ -365,69 +377,45 @@ export async function acceptPeerPushPayment(
const peerInc = await ws.db
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
.runReadOnly(async (tx) => {
- return tx.peerPushPaymentIncoming.get([
- req.exchangeBaseUrl,
- req.pursePub,
- ]);
+ return tx.peerPushPaymentIncoming.get(req.peerPushPaymentIncomingId);
});
if (!peerInc) {
- throw Error("can't accept unknown incoming p2p push payment");
+ throw Error(
+ `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
+ );
}
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
- // We have to create the key pair outside of the transaction,
+ // We have to eagerly create the key pair outside of the transaction,
// due to the async crypto API.
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
- const reserve: ReserveRecord | undefined = await ws.db
+ const mergeReserveInfo: MergeReserveInfo = await ws.db
.mktx((x) => ({
exchanges: x.exchanges,
- reserves: x.reserves,
+ withdrawalGroups: x.withdrawalGroups,
}))
.runReadWrite(async (tx) => {
- const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ const ex = await tx.exchanges.get(peerInc.exchangeBaseUrl);
checkDbInvariant(!!ex);
- if (ex.currentMergeReservePub) {
- return await tx.reserves.get(ex.currentMergeReservePub);
+ if (ex.currentMergeReserveInfo) {
+ return ex.currentMergeReserveInfo;
}
- const rec: ReserveRecord = {
- exchangeBaseUrl: req.exchangeBaseUrl,
- // FIXME: field will be removed in the future, folded into withdrawal/p2p record.
- reserveStatus: ReserveRecordStatus.Dormant,
- timestampCreated: TalerProtocolTimestamp.now(),
- instructedAmount: Amounts.getZero(amount.currency),
- currency: amount.currency,
- reservePub: newReservePair.pub,
+ await tx.exchanges.put(ex);
+ ex.currentMergeReserveInfo = {
reservePriv: newReservePair.priv,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- // FIXME!
- initialDenomSel: undefined as any,
- // FIXME!
- initialWithdrawalGroupId: "",
- initialWithdrawalStarted: false,
- lastError: undefined,
- operationStatus: OperationStatus.Pending,
- retryInfo: undefined,
- bankInfo: undefined,
- restrictAge: undefined,
- senderWire: undefined,
+ reservePub: newReservePair.pub,
};
- await tx.reserves.put(rec);
- return rec;
+ return ex.currentMergeReserveInfo;
});
- if (!reserve) {
- throw Error("can't create reserve");
- }
-
const mergeTimestamp = TalerProtocolTimestamp.now();
const reservePayto = talerPaytoFromExchangeReserve(
- reserve.exchangeBaseUrl,
- reserve.reservePub,
+ peerInc.exchangeBaseUrl,
+ mergeReserveInfo.reservePub,
);
const sigRes = await ws.cryptoApi.signPurseMerge({
@@ -442,12 +430,12 @@ export async function acceptPeerPushPayment(
purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
pursePub: peerInc.pursePub,
reservePayto,
- reservePriv: reserve.reservePriv,
+ reservePriv: mergeReserveInfo.reservePriv,
});
const mergePurseUrl = new URL(
- `purses/${req.pursePub}/merge`,
- req.exchangeBaseUrl,
+ `purses/${peerInc.pursePub}/merge`,
+ peerInc.exchangeBaseUrl,
);
const mergeReq: ExchangePurseMergeRequest = {
@@ -459,6 +447,17 @@ export async function acceptPeerPushPayment(
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
+ logger.info(`merge request: ${j2s(mergeReq)}`);
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
- logger.info(`merge result: ${j2s(res)}`);
+ logger.info(`merge response: ${j2s(res)}`);
+
+ await internalCreateWithdrawalGroup(ws, {
+ amount,
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ reserveStatus: ReserveRecordStatus.QueryingStatus,
+ reserveKeyPair: {
+ priv: mergeReserveInfo.reservePriv,
+ pub: mergeReserveInfo.reservePub,
+ },
+ });
}
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 0a262d3bb..ae93711f9 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -70,44 +70,6 @@ async function gatherExchangePending(
});
}
-async function gatherReservePending(
- tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const reserves = await tx.reserves.indexes.byStatus.getAll(
- OperationStatus.Pending,
- );
- for (const reserve of reserves) {
- const reserveType = reserve.bankInfo
- ? ReserveType.TalerBankWithdraw
- : ReserveType.Manual;
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.Dormant:
- // nothing to report as pending
- break;
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.QueryingStatus:
- case ReserveRecordStatus.RegisteringBank: {
- resp.pendingOperations.push({
- type: PendingTaskType.Reserve,
- givesLifeness: true,
- timestampDue: reserve.retryInfo?.nextRetry ?? AbsoluteTime.now(),
- stage: reserve.reserveStatus,
- timestampCreated: reserve.timestampCreated,
- reserveType,
- reservePub: reserve.reservePub,
- retryInfo: reserve.retryInfo,
- });
- break;
- }
- default:
- // FIXME: report problem!
- break;
- }
- }
-}
-
async function gatherRefreshPending(
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
now: AbsoluteTime,
@@ -336,7 +298,6 @@ export async function getPendingOperations(
backupProviders: x.backupProviders,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
- reserves: x.reserves,
refreshGroups: x.refreshGroups,
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
@@ -352,7 +313,6 @@ export async function getPendingOperations(
pendingOperations: [],
};
await gatherExchangePending(tx, now, resp);
- await gatherReservePending(tx, now, resp);
await gatherRefreshPending(tx, now, resp);
await gatherWithdrawalPending(tx, now, resp);
await gatherProposalPending(tx, now, resp);
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index d36a10287..7c0f79daf 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -26,28 +26,35 @@
*/
import {
Amounts,
- codecForRecoupConfirmation, encodeCrock, getRandomBytes, j2s, Logger, NotificationType,
+ codecForRecoupConfirmation,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ Logger,
+ NotificationType,
RefreshReason,
TalerErrorDetail,
- TalerProtocolTimestamp, URL
+ TalerProtocolTimestamp,
+ URL,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
CoinSourceType,
- CoinStatus, OperationStatus, RecoupGroupRecord,
+ CoinStatus,
+ OperationStatus,
+ RecoupGroupRecord,
RefreshCoinSource,
- ReserveRecordStatus, WalletStoresV1, WithdrawCoinSource
+ ReserveRecordStatus,
+ WalletStoresV1,
+ WithdrawCoinSource,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { GetReadWriteAccess } from "../util/query.js";
-import {
- RetryInfo
-} from "../util/retries.js";
+import { RetryInfo } from "../util/retries.js";
import { guardOperationException } from "./common.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
-import { getReserveRequestTimeout, processReserve } from "./reserves.js";
-
+import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/recoup.ts");
@@ -182,34 +189,24 @@ async function recoupWithdrawCoin(
cs: WithdrawCoinSource,
): Promise<void> {
const reservePub = cs.reservePub;
- const d = await ws.db
+ const denomInfo = await ws.db
.mktx((x) => ({
- reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
- const reserve = await tx.reserves.get(reservePub);
- if (!reserve) {
- return;
- }
const denomInfo = await ws.getDenomInfo(
ws,
tx,
- reserve.exchangeBaseUrl,
+ coin.exchangeBaseUrl,
coin.denomPubHash,
);
- if (!denomInfo) {
- return;
- }
- return { reserve, denomInfo };
+ return denomInfo;
});
- if (!d) {
+ if (!denomInfo) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
}
- const { reserve, denomInfo } = d;
-
ws.notify({
type: NotificationType.RecoupStarted,
});
@@ -224,9 +221,7 @@ async function recoupWithdrawCoin(
});
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
logger.trace(`requesting recoup via ${reqUrl.href}`);
- const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
- timeout: getReserveRequestTimeout(reserve),
- });
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
resp,
codecForRecoupConfirmation(),
@@ -244,7 +239,6 @@ async function recoupWithdrawCoin(
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
- reserves: x.reserves,
recoupGroups: x.recoupGroups,
refreshGroups: x.refreshGroups,
}))
@@ -260,18 +254,12 @@ async function recoupWithdrawCoin(
if (!updatedCoin) {
return;
}
- const updatedReserve = await tx.reserves.get(reserve.reservePub);
- if (!updatedReserve) {
- return;
- }
updatedCoin.status = CoinStatus.Dormant;
const currency = updatedCoin.currentAmount.currency;
updatedCoin.currentAmount = Amounts.getZero(currency);
- updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
- updatedReserve.retryInfo = RetryInfo.reset();
- updatedReserve.operationStatus = OperationStatus.Pending;
await tx.coins.put(updatedCoin);
- await tx.reserves.put(updatedReserve);
+ // FIXME: Actually withdraw here!
+ // await internalCreateWithdrawalGroup(ws, {...});
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
});
@@ -341,7 +329,6 @@ async function recoupRefreshCoin(
.mktx((x) => ({
coins: x.coins,
denominations: x.denominations,
- reserves: x.reserves,
recoupGroups: x.recoupGroups,
refreshGroups: x.refreshGroups,
}))
@@ -446,12 +433,6 @@ async function processRecoupGroupImpl(
reserveSet.add(coin.coinSource.reservePub);
}
}
-
- for (const r of reserveSet.values()) {
- processReserve(ws, r, { forceNow: true }).catch((e) => {
- logger.error(`processing reserve ${r} after recoup failed`);
- });
- }
}
export async function createRecoupGroup(
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
deleted file mode 100644
index b33f574f4..000000000
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ /dev/null
@@ -1,843 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AbsoluteTime,
- AcceptWithdrawalResponse,
- addPaytoQueryParams,
- Amounts,
- canonicalizeBaseUrl,
- codecForBankWithdrawalOperationPostResponse,
- codecForReserveStatus,
- codecForWithdrawOperationStatusResponse,
- CreateReserveRequest,
- CreateReserveResponse,
- Duration,
- durationMax,
- durationMin,
- encodeCrock,
- ForcedDenomSel,
- getRandomBytes,
- j2s,
- Logger,
- NotificationType,
- randomBytes,
- TalerErrorCode,
- TalerErrorDetail,
- URL,
-} from "@gnu-taler/taler-util";
-import {
- DenomSelectionState,
- OperationStatus,
- ReserveBankInfo,
- ReserveRecord,
- ReserveRecordStatus,
- WalletStoresV1,
- WithdrawalGroupRecord,
-} from "../db.js";
-import { TalerError } from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import {
- readSuccessResponseJsonOrErrorCode,
- readSuccessResponseJsonOrThrow,
- throwUnexpectedRequestError,
-} from "../util/http.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-import { RetryInfo } from "../util/retries.js";
-import { guardOperationException } from "./common.js";
-import {
- getExchangeDetails,
- getExchangePaytoUri,
- getExchangeTrust,
- updateExchangeFromUrl,
-} from "./exchanges.js";
-import {
- getBankWithdrawalInfo,
- getCandidateWithdrawalDenoms,
- processWithdrawGroup,
- selectForcedWithdrawalDenominations,
- selectWithdrawalDenominations,
- updateWithdrawalDenoms,
-} from "./withdraw.js";
-
-const logger = new Logger("taler-wallet-core:reserves.ts");
-
-/**
- * Set up the reserve's retry timeout in preparation for
- * processing the reserve.
- */
-async function setupReserveRetry(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- reset: boolean;
- },
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- if (options.reset) {
- r.retryInfo = RetryInfo.reset();
- } else {
- r.retryInfo = RetryInfo.increment(r.retryInfo);
- }
- delete r.lastError;
- await tx.reserves.put(r);
- });
-}
-
-/**
- * Report an error that happened while processing the reserve.
- *
- * Logs the error via a notification and by storing it in the database.
- */
-async function reportReserveError(
- ws: InternalWalletState,
- reservePub: string,
- err: TalerErrorDetail,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- logger.error(`got reserve error for inactive reserve (no retryInfo)`);
- return;
- }
- r.lastError = err;
- await tx.reserves.put(r);
- });
- ws.notify({
- type: NotificationType.ReserveOperationError,
- error: err,
- });
-}
-
-/**
- * Create a reserve, but do not flag it as confirmed yet.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- */
-export async function createReserve(
- ws: InternalWalletState,
- req: CreateReserveRequest,
-): Promise<CreateReserveResponse> {
- const keypair = await ws.cryptoApi.createEddsaKeypair({});
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- const canonExchange = canonicalizeBaseUrl(req.exchange);
-
- let reserveStatus;
- if (req.bankWithdrawStatusUrl) {
- reserveStatus = ReserveRecordStatus.RegisteringBank;
- } else {
- reserveStatus = ReserveRecordStatus.QueryingStatus;
- }
-
- let bankInfo: ReserveBankInfo | undefined;
-
- if (req.bankWithdrawStatusUrl) {
- if (!req.exchangePaytoUri) {
- throw Error(
- "Exchange payto URI must be specified for a bank-integrated withdrawal",
- );
- }
- bankInfo = {
- statusUrl: req.bankWithdrawStatusUrl,
- exchangePaytoUri: req.exchangePaytoUri,
- };
- }
-
- const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
-
- await updateWithdrawalDenoms(ws, canonExchange);
- const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
-
- let initialDenomSel: DenomSelectionState;
- if (req.forcedDenomSel) {
- logger.warn("using forced denom selection");
- initialDenomSel = selectForcedWithdrawalDenominations(
- req.amount,
- denoms,
- req.forcedDenomSel,
- );
- } else {
- initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
- }
-
- const reserveRecord: ReserveRecord = {
- instructedAmount: req.amount,
- initialWithdrawalGroupId,
- initialDenomSel,
- initialWithdrawalStarted: false,
- timestampCreated: now,
- exchangeBaseUrl: canonExchange,
- reservePriv: keypair.priv,
- reservePub: keypair.pub,
- senderWire: req.senderWire,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- bankInfo,
- reserveStatus,
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- currency: req.amount.currency,
- operationStatus: OperationStatus.Pending,
- restrictAge: req.restrictAge,
- };
-
- const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
- const exchangeDetails = exchangeInfo.exchangeDetails;
- if (!exchangeDetails) {
- logger.trace(exchangeDetails);
- throw Error("exchange not updated");
- }
- const { isAudited, isTrusted } = await getExchangeTrust(
- ws,
- exchangeInfo.exchange,
- );
-
- const resp = await ws.db
- .mktx((x) => ({
- exchangeTrust: x.exchangeTrust,
- reserves: x.reserves,
- bankWithdrawUris: x.bankWithdrawUris,
- }))
- .runReadWrite(async (tx) => {
- // Check if we have already created a reserve for that bankWithdrawStatusUrl
- if (reserveRecord.bankInfo?.statusUrl) {
- const bwi = await tx.bankWithdrawUris.get(
- reserveRecord.bankInfo.statusUrl,
- );
- if (bwi) {
- const otherReserve = await tx.reserves.get(bwi.reservePub);
- if (otherReserve) {
- logger.trace(
- "returning existing reserve for bankWithdrawStatusUri",
- );
- return {
- exchange: otherReserve.exchangeBaseUrl,
- reservePub: otherReserve.reservePub,
- };
- }
- }
- await tx.bankWithdrawUris.put({
- reservePub: reserveRecord.reservePub,
- talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
- });
- }
- if (!isAudited && !isTrusted) {
- await tx.exchangeTrust.put({
- currency: reserveRecord.currency,
- exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
- exchangeMasterPub: exchangeDetails.masterPublicKey,
- uids: [encodeCrock(getRandomBytes(32))],
- });
- }
- await tx.reserves.put(reserveRecord);
- const r: CreateReserveResponse = {
- exchange: canonExchange,
- reservePub: keypair.pub,
- };
- return r;
- });
-
- if (reserveRecord.reservePub === resp.reservePub) {
- // Only emit notification when a new reserve was created.
- ws.notify({
- type: NotificationType.ReserveCreated,
- reservePub: reserveRecord.reservePub,
- });
- }
-
- // Asynchronously process the reserve, but return
- // to the caller already.
- processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => {
- logger.error("Processing reserve (after createReserve) failed:", e);
- });
-
- return resp;
-}
-
-/**
- * Re-query the status of a reserve.
- */
-export async function forceQueryReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const reserve = await tx.reserves.get(reservePub);
- if (!reserve) {
- return;
- }
- // Only force status query where it makes sense
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.Dormant:
- reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
- reserve.operationStatus = OperationStatus.Pending;
- reserve.retryInfo = RetryInfo.reset();
- break;
- default:
- break;
- }
- await tx.reserves.put(reserve);
- });
- await processReserve(ws, reservePub, { forceNow: true });
-}
-
-/**
- * First fetch information required to withdraw from the reserve,
- * then deplete the reserve, withdrawing coins until it is empty.
- *
- * The returned promise resolves once the reserve is set to the
- * state "Dormant".
- */
-export async function processReserve(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<void> {
- return ws.memoProcessReserve.memo(reservePub, async () => {
- const onOpError = (err: TalerErrorDetail): Promise<void> =>
- reportReserveError(ws, reservePub, err);
- await guardOperationException(
- () => processReserveImpl(ws, reservePub, options),
- onOpError,
- );
- });
-}
-
-async function registerReserveWithBank(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return await tx.reserves.get(reservePub);
- });
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.RegisteringBank:
- break;
- default:
- return;
- }
- const bankInfo = reserve.bankInfo;
- if (!bankInfo) {
- return;
- }
- const bankStatusUrl = bankInfo.statusUrl;
- const httpResp = await ws.http.postJson(
- bankStatusUrl,
- {
- reserve_pub: reservePub,
- selected_exchange: bankInfo.exchangePaytoUri,
- },
- {
- timeout: getReserveRequestTimeout(reserve),
- },
- );
- await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForBankWithdrawalOperationPostResponse(),
- );
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- r.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
- AbsoluteTime.now(),
- );
- r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
- r.operationStatus = OperationStatus.Pending;
- if (!r.bankInfo) {
- throw Error("invariant failed");
- }
- r.retryInfo = RetryInfo.reset();
- await tx.reserves.put(r);
- });
- ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
- return processReserveBankStatus(ws, reservePub);
-}
-
-export function getReserveRequestTimeout(r: ReserveRecord): Duration {
- return durationMax(
- { d_ms: 60000 },
- durationMin({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
- );
-}
-
-async function processReserveBankStatus(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<void> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- switch (reserve?.reserveStatus) {
- case ReserveRecordStatus.WaitConfirmBank:
- case ReserveRecordStatus.RegisteringBank:
- break;
- default:
- return;
- }
- const bankStatusUrl = reserve.bankInfo?.statusUrl;
- if (!bankStatusUrl) {
- return;
- }
-
- const statusResp = await ws.http.get(bankStatusUrl, {
- timeout: getReserveRequestTimeout(reserve),
- });
- const status = await readSuccessResponseJsonOrThrow(
- statusResp,
- codecForWithdrawOperationStatusResponse(),
- );
-
- if (status.aborted) {
- logger.info("bank aborted the withdrawal");
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.timestampBankConfirmed = now;
- r.reserveStatus = ReserveRecordStatus.BankAborted;
- r.operationStatus = OperationStatus.Finished;
- r.retryInfo = RetryInfo.reset();
- await tx.reserves.put(r);
- });
- return;
- }
-
- // Bank still needs to know our reserve info
- if (!status.selection_done) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
-
- // FIXME: Why do we do this?!
- if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
- await registerReserveWithBank(ws, reservePub);
- return await processReserveBankStatus(ws, reservePub);
- }
-
- await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- return;
- }
- // Re-check reserve status within transaction
- switch (r.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- case ReserveRecordStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- if (status.transfer_done) {
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.timestampBankConfirmed = now;
- r.reserveStatus = ReserveRecordStatus.QueryingStatus;
- r.operationStatus = OperationStatus.Pending;
- r.retryInfo = RetryInfo.reset();
- } else {
- logger.info("Withdrawal operation not yet confirmed by bank");
- if (r.bankInfo) {
- r.bankInfo.confirmUrl = status.confirm_transfer_url;
- }
- r.retryInfo = RetryInfo.increment(r.retryInfo);
- }
- await tx.reserves.put(r);
- });
-}
-
-/**
- * Update the information about a reserve that is stored in the wallet
- * by querying the reserve's exchange.
- *
- * If the reserve have funds that are not allocated in a withdrawal group yet
- * and are big enough to withdraw with available denominations,
- * create a new withdrawal group for the remaining amount.
- */
-async function updateReserve(
- ws: InternalWalletState,
- reservePub: string,
-): Promise<{ ready: boolean }> {
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- if (!reserve) {
- throw Error("reserve not in db");
- }
-
- if (reserve.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
- return { ready: true };
- }
-
- const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
- reserveUrl.searchParams.set("timeout_ms", "30000");
-
- logger.info(`querying reserve status via ${reserveUrl}`);
-
- const resp = await ws.http.get(reserveUrl.href, {
- timeout: getReserveRequestTimeout(reserve),
- });
-
- const result = await readSuccessResponseJsonOrErrorCode(
- resp,
- codecForReserveStatus(),
- );
-
- if (result.isError) {
- if (
- resp.status === 404 &&
- result.talerErrorResponse.code ===
- TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
- ) {
- ws.notify({
- type: NotificationType.ReserveNotYetFound,
- reservePub,
- });
- return { ready: false };
- } else {
- throwUnexpectedRequestError(resp, result.talerErrorResponse);
- }
- }
-
- logger.trace(`got reserve status ${j2s(result.response)}`);
-
- const reserveInfo = result.response;
- const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance);
- const currency = reserveBalance.currency;
-
- await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
- const denoms = await getCandidateWithdrawalDenoms(
- ws,
- reserve.exchangeBaseUrl,
- );
-
- const newWithdrawalGroup = await ws.db
- .mktx((x) => ({
- planchets: x.planchets,
- withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
- denominations: x.denominations,
- }))
- .runReadWrite(async (tx) => {
- const newReserve = await tx.reserves.get(reserve.reservePub);
- if (!newReserve) {
- return;
- }
-
- let amountReservePlus = reserveBalance;
- let amountReserveMinus = Amounts.getZero(currency);
-
- // Subtract amount allocated in unfinished withdrawal groups
- // for this reserve from the available amount.
- await tx.withdrawalGroups.indexes.byReservePub
- .iter(reservePub)
- .forEachAsync(async (wg) => {
- if (wg.timestampFinish) {
- return;
- }
- await tx.planchets.indexes.byGroup
- .iter(wg.withdrawalGroupId)
- .forEachAsync(async (pr) => {
- if (pr.withdrawalDone) {
- return;
- }
- const denomInfo = await ws.getDenomInfo(
- ws,
- tx,
- wg.exchangeBaseUrl,
- pr.denomPubHash,
- );
- if (!denomInfo) {
- logger.error(`no denom info found for ${pr.denomPubHash}`);
- return;
- }
- amountReserveMinus = Amounts.add(
- amountReserveMinus,
- denomInfo.value,
- denomInfo.feeWithdraw,
- ).amount;
- });
- });
-
- const remainingAmount = Amounts.sub(
- amountReservePlus,
- amountReserveMinus,
- ).amount;
-
- let withdrawalGroupId: string;
- let denomSel: DenomSelectionState;
-
- if (!newReserve.initialWithdrawalStarted) {
- withdrawalGroupId = newReserve.initialWithdrawalGroupId;
- newReserve.initialWithdrawalStarted = true;
- denomSel = newReserve.initialDenomSel;
- } else {
- withdrawalGroupId = encodeCrock(randomBytes(32));
-
- denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
-
- logger.trace(
- `Remaining unclaimed amount in reseve is ${Amounts.stringify(
- remainingAmount,
- )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
- );
-
- if (denomSel.selectedDenoms.length === 0) {
- newReserve.reserveStatus = ReserveRecordStatus.Dormant;
- newReserve.operationStatus = OperationStatus.Finished;
- delete newReserve.lastError;
- delete newReserve.retryInfo;
- await tx.reserves.put(newReserve);
- return;
- }
- }
-
- const withdrawalRecord: WithdrawalGroupRecord = {
- withdrawalGroupId: withdrawalGroupId,
- exchangeBaseUrl: reserve.exchangeBaseUrl,
- reservePub: reserve.reservePub,
- rawWithdrawalAmount: remainingAmount,
- timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- retryInfo: RetryInfo.reset(),
- lastError: undefined,
- denomsSel: denomSel,
- secretSeed: encodeCrock(getRandomBytes(64)),
- denomSelUid: encodeCrock(getRandomBytes(32)),
- operationStatus: OperationStatus.Pending,
- };
-
- delete newReserve.lastError;
- delete newReserve.retryInfo;
- newReserve.reserveStatus = ReserveRecordStatus.Dormant;
- newReserve.operationStatus = OperationStatus.Finished;
-
- await tx.reserves.put(newReserve);
- await tx.withdrawalGroups.put(withdrawalRecord);
- return withdrawalRecord;
- });
-
- if (newWithdrawalGroup) {
- logger.trace("processing new withdraw group");
- ws.notify({
- type: NotificationType.WithdrawGroupCreated,
- withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
- });
- await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
- }
-
- return { ready: true };
-}
-
-async function processReserveImpl(
- ws: InternalWalletState,
- reservePub: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<void> {
- const forceNow = options.forceNow ?? false;
- await setupReserveRetry(ws, reservePub, { reset: forceNow });
- const reserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reservePub);
- });
- if (!reserve) {
- logger.error(
- `not processing reserve: reserve ${reservePub} does not exist`,
- );
- return;
- }
- logger.trace(
- `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
- );
- switch (reserve.reserveStatus) {
- case ReserveRecordStatus.RegisteringBank:
- await processReserveBankStatus(ws, reservePub);
- return await processReserveImpl(ws, reservePub, { forceNow: true });
- case ReserveRecordStatus.QueryingStatus: {
- const res = await updateReserve(ws, reservePub);
- if (res.ready) {
- return await processReserveImpl(ws, reservePub, { forceNow: true });
- }
- break;
- }
- case ReserveRecordStatus.Dormant:
- // nothing to do
- break;
- case ReserveRecordStatus.WaitConfirmBank:
- await processReserveBankStatus(ws, reservePub);
- break;
- case ReserveRecordStatus.BankAborted:
- break;
- default:
- console.warn("unknown reserve record status:", reserve.reserveStatus);
- assertUnreachable(reserve.reserveStatus);
- break;
- }
-}
-
-/**
- * Create a reserve for a bank-integrated withdrawal from
- * a taler://withdraw URI.
- */
-export async function createTalerWithdrawReserve(
- ws: InternalWalletState,
- talerWithdrawUri: string,
- selectedExchange: string,
- options: {
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
- } = {},
-): Promise<AcceptWithdrawalResponse> {
- await updateExchangeFromUrl(ws, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
- const exchangePaytoUri = await getExchangePaytoUri(
- ws,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
- const reserve = await createReserve(ws, {
- amount: withdrawInfo.amount,
- bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
- exchange: selectedExchange,
- senderWire: withdrawInfo.senderWire,
- exchangePaytoUri: exchangePaytoUri,
- restrictAge: options.restrictAge,
- forcedDenomSel: options.forcedDenomSel,
- });
- // We do this here, as the reserve should be registered before we return,
- // so that we can redirect the user to the bank's status page.
- await processReserveBankStatus(ws, reserve.reservePub);
- const processedReserve = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.get(reserve.reservePub);
- });
- if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- }
- return {
- reservePub: reserve.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- };
-}
-
-/**
- * Get payto URIs that can be used to fund a reserve.
- */
-export async function getFundingPaytoUris(
- tx: GetReadOnlyAccess<{
- reserves: typeof WalletStoresV1.reserves;
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- reservePub: string,
-): Promise<string[]> {
- const r = await tx.reserves.get(reservePub);
- if (!r) {
- logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
- return [];
- }
- const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
- if (!exchangeDetails) {
- logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
- return [];
- }
- const plainPaytoUris =
- exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
- if (!plainPaytoUris) {
- logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
- return [];
- }
- return plainPaytoUris.map((x) =>
- addPaytoQueryParams(x, {
- amount: Amounts.stringify(r.instructedAmount),
- message: `Taler Withdrawal ${r.reservePub}`,
- }),
- );
-}
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index d609011ca..bec8ec8f8 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -39,12 +39,12 @@ import {
URL,
PreparePayResultType,
} from "@gnu-taler/taler-util";
-import { createTalerWithdrawReserve } from "./reserves.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { confirmPay, preparePayForUri } from "./pay.js";
import { getBalances } from "./balance.js";
import { applyRefund } from "./refund.js";
import { checkLogicInvariant } from "../util/invariants.js";
+import { acceptWithdrawalFromUri } from "./withdraw.js";
const logger = new Logger("operations/testing.ts");
@@ -104,14 +104,11 @@ export async function withdrawTestBalance(
amount,
);
- await createTalerWithdrawReserve(
- ws,
- wresp.taler_withdraw_uri,
- exchangeBaseUrl,
- {
- forcedDenomSel: req.forcedDenomSel,
- },
- );
+ await acceptWithdrawalFromUri(ws, {
+ talerWithdrawUri: wresp.taler_withdraw_uri,
+ selectedExchange: exchangeBaseUrl,
+ forcedDenomSel: req.forcedDenomSel,
+ });
await confirmBankWithdrawalUri(
ws.http,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index ebc223b23..ae4ce6999 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -36,7 +36,6 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import {
AbortStatus,
RefundState,
- ReserveRecord,
ReserveRecordStatus,
WalletRefundItem,
} from "../db.js";
@@ -44,9 +43,8 @@ import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js";
import { processPurchasePay } from "./pay.js";
import { processRefreshGroup } from "./refresh.js";
-import { getFundingPaytoUris } from "./reserves.js";
import { processTip } from "./tip.js";
-import { processWithdrawGroup } from "./withdraw.js";
+import { processWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
@@ -127,7 +125,6 @@ export async function getTransactions(
proposals: x.proposals,
purchases: x.purchases,
refreshGroups: x.refreshGroups,
- reserves: x.reserves,
tips: x.tips,
withdrawalGroups: x.withdrawalGroups,
planchets: x.planchets,
@@ -151,24 +148,13 @@ export async function getTransactions(
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
-
- const r = await tx.reserves.get(wsr.reservePub);
- if (!r) {
- return;
- }
- let amountRaw: AmountJson | undefined = undefined;
- if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
- amountRaw = r.instructedAmount;
- } else {
- amountRaw = wsr.denomsSel.totalWithdrawCost;
- }
let withdrawalDetails: WithdrawalDetails;
- if (r.bankInfo) {
+ if (wsr.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: r.timestampBankConfirmed ? true : false,
+ confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false,
reservePub: wsr.reservePub,
- bankConfirmationUrl: r.bankInfo.confirmUrl,
+ bankConfirmationUrl: wsr.bankInfo.confirmUrl,
};
} else {
const exchangeDetails = await getExchangeDetails(
@@ -191,7 +177,7 @@ export async function getTransactions(
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(amountRaw),
+ amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
@@ -205,56 +191,6 @@ export async function getTransactions(
});
});
- // Report pending withdrawals based on reserves that
- // were created, but where the actual withdrawal group has
- // not started yet.
- tx.reserves.iter().forEachAsync(async (r) => {
- if (shouldSkipCurrency(transactionsRequest, r.currency)) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- if (r.initialWithdrawalStarted) {
- return;
- }
- if (r.reserveStatus === ReserveRecordStatus.BankAborted) {
- return;
- }
- let withdrawalDetails: WithdrawalDetails;
- if (r.bankInfo) {
- withdrawalDetails = {
- type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: false,
- reservePub: r.reservePub,
- bankConfirmationUrl: r.bankInfo.confirmUrl,
- };
- } else {
- withdrawalDetails = {
- type: WithdrawalType.ManualTransfer,
- reservePub: r.reservePub,
- exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
- };
- }
- transactions.push({
- type: TransactionType.Withdrawal,
- amountRaw: Amounts.stringify(r.instructedAmount),
- amountEffective: Amounts.stringify(
- r.initialDenomSel.totalCoinValue,
- ),
- exchangeBaseUrl: r.exchangeBaseUrl,
- pending: true,
- timestamp: r.timestampCreated,
- withdrawalDetails: withdrawalDetails,
- transactionId: makeEventId(
- TransactionType.Withdrawal,
- r.initialWithdrawalGroupId,
- ),
- frozen: false,
- ...(r.lastError ? { error: r.lastError } : {}),
- });
- });
-
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
@@ -499,7 +435,7 @@ export async function retryTransaction(
}
case TransactionType.Withdrawal: {
const withdrawalGroupId = rest[0];
- await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true });
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true });
break;
}
case TransactionType.Payment: {
@@ -536,7 +472,6 @@ export async function deleteTransaction(
await ws.db
.mktx((x) => ({
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
tombstones: x.tombstones,
}))
.runReadWrite(async (tx) => {
@@ -550,17 +485,6 @@ export async function deleteTransaction(
});
return;
}
- const reserveRecord: ReserveRecord | undefined =
- await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
- withdrawalGroupId,
- );
- if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
- const reservePub = reserveRecord.reservePub;
- await tx.reserves.delete(reservePub);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteReserve + ":" + reservePub,
- });
- }
});
} else if (type === TransactionType.Payment) {
const proposalId = rest[0];
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index ea9e22331..484b9b962 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -19,20 +19,29 @@
*/
import {
AbsoluteTime,
+ AcceptManualWithdrawalResult,
+ AcceptWithdrawalResponse,
+ addPaytoQueryParams,
AmountJson,
+ AmountLike,
Amounts,
AmountString,
BankWithdrawDetails,
+ canonicalizeBaseUrl,
+ codecForBankWithdrawalOperationPostResponse,
+ codecForReserveStatus,
codecForTalerConfigResponse,
codecForWithdrawBatchResponse,
codecForWithdrawOperationStatusResponse,
codecForWithdrawResponse,
DenomKeyType,
Duration,
- durationFromSpec,
+ durationFromSpec, encodeCrock,
ExchangeListItem,
ExchangeWithdrawRequest,
ForcedDenomSel,
+ getRandomBytes,
+ j2s,
LibtoolVersion,
Logger,
NotificationType,
@@ -45,8 +54,9 @@ import {
VersionMatchResult,
WithdrawBatchResponse,
WithdrawResponse,
- WithdrawUriInfoResponse,
+ WithdrawUriInfoResponse
} from "@gnu-taler/taler-util";
+import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
import {
CoinRecord,
CoinSourceType,
@@ -58,26 +68,42 @@ import {
ExchangeRecord,
OperationStatus,
PlanchetRecord,
- WithdrawalGroupRecord,
+ ReserveBankInfo,
+ ReserveRecordStatus,
+ WalletStoresV1,
+ WithdrawalGroupRecord
} from "../db.js";
import {
getErrorDetailFromException,
makeErrorDetail,
- TalerError,
+ TalerError
} from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js";
import {
HttpRequestLibrary,
+ readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
+ throwUnexpectedRequestError
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import {
+ DbAccess,
+ GetReadOnlyAccess
+} from "../util/query.js";
import { RetryInfo } from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
+ WALLET_EXCHANGE_PROTOCOL_VERSION
} from "../versions.js";
import { guardOperationException } from "./common.js";
+import {
+ getExchangeDetails,
+ getExchangePaytoUri,
+ getExchangeTrust,
+ updateExchangeFromUrl
+} from "./exchanges.js";
/**
* Logger for this file.
@@ -215,7 +241,7 @@ export function selectWithdrawalDenominations(
for (const d of denoms) {
let count = 0;
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
- for (; ;) {
+ for (;;) {
if (Amounts.cmp(remaining, cost) < 0) {
break;
}
@@ -410,47 +436,42 @@ async function processPlanchetGenerate(
return;
}
let ci = 0;
- let denomPubHash: string | undefined;
+ let maybeDenomPubHash: string | undefined;
for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
if (coinIdx >= ci && coinIdx < ci + d.count) {
- denomPubHash = d.denomPubHash;
+ maybeDenomPubHash = d.denomPubHash;
break;
}
ci += d.count;
}
- if (!denomPubHash) {
+ if (!maybeDenomPubHash) {
throw Error("invariant violated");
}
+ const denomPubHash = maybeDenomPubHash;
- const { denom, reserve } = await ws.db
+ const denom = await ws.db
.mktx((x) => ({
- reserves: x.reserves,
denominations: x.denominations,
}))
.runReadOnly(async (tx) => {
- const denom = await tx.denominations.get([
+ return ws.getDenomInfo(
+ ws,
+ tx,
withdrawalGroup.exchangeBaseUrl,
- denomPubHash!,
- ]);
- if (!denom) {
- throw Error("invariant violated");
- }
- const reserve = await tx.reserves.get(withdrawalGroup.reservePub);
- if (!reserve) {
- throw Error("invariant violated");
- }
- return { denom, reserve };
+ denomPubHash,
+ );
});
+ checkDbInvariant(!!denom);
const r = await ws.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw,
- reservePriv: reserve.reservePriv,
- reservePub: reserve.reservePub,
+ reservePriv: withdrawalGroup.reservePriv,
+ reservePub: withdrawalGroup.reservePub,
value: denom.value,
coinIndex: coinIdx,
secretSeed: withdrawalGroup.secretSeed,
- restrictAge: reserve.restrictAge,
+ restrictAge: withdrawalGroup.restrictAge,
});
const newPlanchet: PlanchetRecord = {
blindingKey: r.blindingKey,
@@ -806,11 +827,13 @@ async function processPlanchetVerifyAndStoreCoin(
const planchetCoinPub = planchet.coinPub;
+ // Check if this is the first time that the whole
+ // withdrawal succeeded. If so, mark the withdrawal
+ // group as finished.
const firstSuccess = await ws.db
.mktx((x) => ({
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
@@ -875,7 +898,8 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified
) {
logger.trace(
- `Validating denomination (${current + 1}/${denominations.length
+ `Validating denomination (${current + 1}/${
+ denominations.length
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
@@ -960,7 +984,80 @@ async function reportWithdrawalError(
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
}
-export async function processWithdrawGroup(
+/**
+ * Update the information about a reserve that is stored in the wallet
+ * by querying the reserve's exchange.
+ *
+ * If the reserve have funds that are not allocated in a withdrawal group yet
+ * and are big enough to withdraw with available denominations,
+ * create a new withdrawal group for the remaining amount.
+ */
+async function queryReserve(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<{ ready: boolean }> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ checkDbInvariant(!!withdrawalGroup);
+ if (withdrawalGroup.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
+ return { ready: true };
+ }
+ const reservePub = withdrawalGroup.reservePub;
+
+ const reserveUrl = new URL(
+ `reserves/${reservePub}`,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+
+ logger.info(`querying reserve status via ${reserveUrl}`);
+
+ const resp = await ws.http.get(reserveUrl.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ });
+
+ const result = await readSuccessResponseJsonOrErrorCode(
+ resp,
+ codecForReserveStatus(),
+ );
+
+ if (result.isError) {
+ if (
+ resp.status === 404 &&
+ result.talerErrorResponse.code ===
+ TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
+ ) {
+ ws.notify({
+ type: NotificationType.ReserveNotYetFound,
+ reservePub,
+ });
+ return { ready: false };
+ } else {
+ throwUnexpectedRequestError(resp, result.talerErrorResponse);
+ }
+ }
+
+ logger.trace(`got reserve status ${j2s(result.response)}`);
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ wg.reserveStatus = ReserveRecordStatus.Dormant;
+ await tx.withdrawalGroups.put(wg);
+ });
+
+ return { ready: true };
+}
+
+export async function processWithdrawalGroup(
ws: InternalWalletState,
withdrawalGroupId: string,
options: {
@@ -990,24 +1087,42 @@ async function processWithdrawGroupImpl(
.runReadOnly(async (tx) => {
return tx.withdrawalGroups.get(withdrawalGroupId);
});
+
if (!withdrawalGroup) {
- // Withdrawal group doesn't exist yet, but reserve might exist
- // (and reference the yet to be created withdrawal group)
- const reservePub = await ws.db
- .mktx((x) => ({ reserves: x.reserves }))
- .runReadOnly(async (tx) => {
- const r = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
- withdrawalGroupId,
- );
- return r?.reservePub;
+ throw Error(`withdrawal group ${withdrawalGroupId} not found`);
+ }
+
+ switch (withdrawalGroup.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
+ forceNow: true,
});
- if (!reservePub) {
- logger.warn(
- "withdrawal group doesn't exist (and reserve doesn't exist either)",
- );
+ case ReserveRecordStatus.QueryingStatus: {
+ const res = await queryReserve(ws, withdrawalGroupId);
+ if (res.ready) {
+ return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
+ forceNow: true,
+ });
+ }
return;
}
- return await ws.reserveOps.processReserve(ws, reservePub, { forceNow });
+ case ReserveRecordStatus.WaitConfirmBank:
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ return;
+ case ReserveRecordStatus.BankAborted:
+ // FIXME
+ return;
+ case ReserveRecordStatus.Dormant:
+ // We can try to withdraw, nothing needs to be done with the reserve.
+ break;
+ default:
+ logger.warn(
+ "unknown reserve record status:",
+ withdrawalGroup.reserveStatus,
+ );
+ assertUnreachable(withdrawalGroup.reserveStatus);
+ break;
}
await ws.exchangeOps.updateExchangeFromUrl(
@@ -1071,7 +1186,6 @@ async function processWithdrawGroupImpl(
.mktx((x) => ({
coins: x.coins,
withdrawalGroups: x.withdrawalGroups,
- reserves: x.reserves,
planchets: x.planchets,
}))
.runReadWrite(async (tx) => {
@@ -1200,9 +1314,9 @@ export async function getExchangeWithdrawalInfo(
!versionMatch.compatible &&
versionMatch.currentCmp === -1
) {
- console.warn(
+ logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
+ `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
);
}
}
@@ -1308,3 +1422,456 @@ export async function getWithdrawalDetailsForUri(
possibleExchanges: exchanges,
};
}
+
+export async function getFundingPaytoUrisTx(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<string[]> {
+ return await ws.db
+ .mktx((x) => ({
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId));
+}
+
+/**
+ * Get payto URIs that can be used to fund a withdrawal operation.
+ */
+export async function getFundingPaytoUris(
+ tx: GetReadOnlyAccess<{
+ withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
+ exchanges: typeof WalletStoresV1.exchanges;
+ exchangeDetails: typeof WalletStoresV1.exchangeDetails;
+ }>,
+ withdrawalGroupId: string,
+): Promise<string[]> {
+ const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
+ checkDbInvariant(!!withdrawalGroup);
+ const exchangeDetails = await getExchangeDetails(
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) {
+ logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
+ return [];
+ }
+ const plainPaytoUris =
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+ if (!plainPaytoUris) {
+ logger.error(
+ `exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
+ );
+ return [];
+ }
+ return plainPaytoUris.map((x) =>
+ addPaytoQueryParams(x, {
+ amount: Amounts.stringify(withdrawalGroup.instructedAmount),
+ message: `Taler Withdrawal ${withdrawalGroup.reservePub}`,
+ }),
+ );
+}
+
+async function getWithdrawalGroupRecordTx(
+ db: DbAccess<typeof WalletStoresV1>,
+ req: {
+ withdrawalGroupId: string;
+ },
+): Promise<WithdrawalGroupRecord | undefined> {
+ return await db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadOnly(async (tx) => {
+ return tx.withdrawalGroups.get(req.withdrawalGroupId);
+ });
+}
+
+export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
+ return Duration.max(
+ { d_ms: 60000 },
+ Duration.min({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
+ );
+}
+
+async function registerReserveWithBank(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const withdrawalGroup = await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadOnly(async (tx) => {
+ return await tx.withdrawalGroups.get(withdrawalGroupId);
+ });
+ switch (withdrawalGroup?.reserveStatus) {
+ case ReserveRecordStatus.WaitConfirmBank:
+ case ReserveRecordStatus.RegisteringBank:
+ break;
+ default:
+ return;
+ }
+ const bankInfo = withdrawalGroup.bankInfo;
+ if (!bankInfo) {
+ return;
+ }
+ const bankStatusUrl = bankInfo.statusUrl;
+ const httpResp = await ws.http.postJson(
+ bankStatusUrl,
+ {
+ reserve_pub: withdrawalGroup.reservePub,
+ selected_exchange: bankInfo.exchangePaytoUri,
+ },
+ {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ },
+ );
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForBankWithdrawalOperationPostResponse(),
+ );
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ r.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
+ AbsoluteTime.now(),
+ );
+ r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
+ r.operationStatus = OperationStatus.Pending;
+ r.retryInfo = RetryInfo.reset();
+ await tx.withdrawalGroups.put(r);
+ });
+ ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
+ return processReserveBankStatus(ws, withdrawalGroupId);
+}
+
+async function processReserveBankStatus(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ switch (withdrawalGroup?.reserveStatus) {
+ case ReserveRecordStatus.WaitConfirmBank:
+ case ReserveRecordStatus.RegisteringBank:
+ break;
+ default:
+ return;
+ }
+ const bankStatusUrl = withdrawalGroup.bankInfo?.statusUrl;
+ if (!bankStatusUrl) {
+ return;
+ }
+
+ const statusResp = await ws.http.get(bankStatusUrl, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ });
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (status.aborted) {
+ logger.info("bank aborted the withdrawal");
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ r.bankInfo.timestampBankConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.BankAborted;
+ r.operationStatus = OperationStatus.Finished;
+ r.retryInfo = RetryInfo.reset();
+ await tx.withdrawalGroups.put(r);
+ });
+ return;
+ }
+
+ // Bank still needs to know our reserve info
+ if (!status.selection_done) {
+ await registerReserveWithBank(ws, withdrawalGroupId);
+ return await processReserveBankStatus(ws, withdrawalGroupId);
+ }
+
+ // FIXME: Why do we do this?!
+ if (withdrawalGroup.reserveStatus === ReserveRecordStatus.RegisteringBank) {
+ await registerReserveWithBank(ws, withdrawalGroupId);
+ return await processReserveBankStatus(ws, withdrawalGroupId);
+ }
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ }))
+ .runReadWrite(async (tx) => {
+ const r = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ // Re-check reserve status within transaction
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.RegisteringBank:
+ case ReserveRecordStatus.WaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (status.transfer_done) {
+ logger.info("withdrawal: transfer confirmed by bank.");
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ r.bankInfo.timestampBankConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.QueryingStatus;
+ r.operationStatus = OperationStatus.Pending;
+ r.retryInfo = RetryInfo.reset();
+ } else {
+ logger.info("withdrawal: transfer not yet confirmed by bank");
+ if (r.bankInfo) {
+ r.bankInfo.confirmUrl = status.confirm_transfer_url;
+ }
+ r.retryInfo = RetryInfo.increment(r.retryInfo);
+ }
+ await tx.withdrawalGroups.put(r);
+ });
+}
+
+export async function internalCreateWithdrawalGroup(
+ ws: InternalWalletState,
+ args: {
+ reserveStatus: ReserveRecordStatus;
+ amount: AmountJson;
+ bankInfo?: ReserveBankInfo;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ reserveKeyPair?: EddsaKeypair;
+ restrictAge?: number;
+ },
+): Promise<WithdrawalGroupRecord> {
+ const reserveKeyPair =
+ args.reserveKeyPair ?? (await ws.cryptoApi.createEddsaKeypair({}));
+ const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ const secretSeed = encodeCrock(getRandomBytes(32));
+ const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
+ const amount = args.amount;
+
+ await updateWithdrawalDenoms(ws, canonExchange);
+ const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
+
+ let initialDenomSel: DenomSelectionState;
+ const denomSelUid = encodeCrock(getRandomBytes(16));
+ if (args.forcedDenomSel) {
+ logger.warn("using forced denom selection");
+ initialDenomSel = selectForcedWithdrawalDenominations(
+ amount,
+ denoms,
+ args.forcedDenomSel,
+ );
+ } else {
+ initialDenomSel = selectWithdrawalDenominations(amount, denoms);
+ }
+
+ const withdrawalGroup: WithdrawalGroupRecord = {
+ denomSelUid,
+ denomsSel: initialDenomSel,
+ exchangeBaseUrl: canonExchange,
+ instructedAmount: amount,
+ timestampStart: now,
+ lastError: undefined,
+ operationStatus: OperationStatus.Pending,
+ rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
+ secretSeed,
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ reserveStatus: args.reserveStatus,
+ retryInfo: RetryInfo.reset(),
+ withdrawalGroupId,
+ bankInfo: args.bankInfo,
+ restrictAge: args.restrictAge,
+ senderWire: undefined,
+ timestampFinish: undefined,
+ };
+
+ const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
+ const exchangeDetails = exchangeInfo.exchangeDetails;
+ if (!exchangeDetails) {
+ logger.trace(exchangeDetails);
+ throw Error("exchange not updated");
+ }
+ const { isAudited, isTrusted } = await getExchangeTrust(
+ ws,
+ exchangeInfo.exchange,
+ );
+
+ await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ exchangeTrust: x.exchangeTrust,
+ }))
+ .runReadWrite(async (tx) => {
+ await tx.withdrawalGroups.add(withdrawalGroup);
+
+ if (!isAudited && !isTrusted) {
+ await tx.exchangeTrust.put({
+ currency: amount.currency,
+ exchangeBaseUrl: canonExchange,
+ exchangeMasterPub: exchangeDetails.masterPublicKey,
+ uids: [encodeCrock(getRandomBytes(32))],
+ });
+ }
+ });
+
+ return withdrawalGroup;
+}
+
+export async function acceptWithdrawalFromUri(
+ ws: InternalWalletState,
+ req: {
+ talerWithdrawUri: string;
+ selectedExchange: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+ },
+): Promise<AcceptWithdrawalResponse> {
+ await updateExchangeFromUrl(ws, req.selectedExchange);
+ const withdrawInfo = await getBankWithdrawalInfo(
+ ws.http,
+ req.talerWithdrawUri,
+ );
+ const exchangePaytoUri = await getExchangePaytoUri(
+ ws,
+ req.selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
+ amount: withdrawInfo.amount,
+ exchangeBaseUrl: req.selectedExchange,
+ forcedDenomSel: req.forcedDenomSel,
+ reserveStatus: ReserveRecordStatus.RegisteringBank,
+ bankInfo: {
+ exchangePaytoUri,
+ statusUrl: withdrawInfo.extractedStatusUrl,
+ confirmUrl: withdrawInfo.confirmTransferUrl,
+ },
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ // We do this here, as the reserve should be registered before we return,
+ // so that we can redirect the user to the bank's status page.
+ await processReserveBankStatus(ws, withdrawalGroupId);
+ const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
+ withdrawalGroupId,
+ });
+ if (
+ processedWithdrawalGroup?.reserveStatus === ReserveRecordStatus.BankAborted
+ ) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
+
+ // Start withdrawal in the background.
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
+ (err) => {
+ logger.error("Processing withdrawal (after creation) failed:", err);
+ },
+ );
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ confirmTransferUrl: withdrawInfo.confirmTransferUrl,
+ };
+}
+
+/**
+ * Create a manual withdrawal operation.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ *
+ * Asynchronously starts the withdrawal.
+ */
+export async function createManualWithdrawal(
+ ws: InternalWalletState,
+ req: {
+ exchangeBaseUrl: string;
+ amount: AmountLike;
+ restrictAge?: number;
+ forcedDenomSel?: ForcedDenomSel;
+ },
+): Promise<AcceptManualWithdrawalResult> {
+ const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
+ amount: Amounts.jsonifyAmount(req.amount),
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ bankInfo: undefined,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ reserveStatus: ReserveRecordStatus.QueryingStatus,
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ const exchangePaytoUris = await ws.db
+ .mktx((x) => ({
+ withdrawalGroups: x.withdrawalGroups,
+ exchanges: x.exchanges,
+ exchangeDetails: x.exchangeDetails,
+ exchangeTrust: x.exchangeTrust,
+ }))
+ .runReadWrite(async (tx) => {
+ return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
+ });
+
+ // Start withdrawal in the background.
+ await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
+ (err) => {
+ logger.error("Processing withdrawal (after creation) failed:", err);
+ },
+ );
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ exchangePaytoUris: exchangePaytoUris,
+ };
+}
diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts
index f4e5216bc..e372a593d 100644
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ b/packages/taler-wallet-core/src/pending-types.ts
@@ -40,7 +40,6 @@ export enum PendingTaskType {
ProposalChoice = "proposal-choice",
ProposalDownload = "proposal-download",
Refresh = "refresh",
- Reserve = "reserve",
Recoup = "recoup",
RefundQuery = "refund-query",
TipPickup = "tip-pickup",
@@ -60,7 +59,6 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
| PendingProposalDownloadTask
| PendingRefreshTask
| PendingRefundQueryTask
- | PendingReserveTask
| PendingTipPickupTask
| PendingWithdrawTask
| PendingRecoupTask
@@ -104,22 +102,6 @@ export enum ReserveType {
}
/**
- * Status of processing a reserve.
- *
- * Does *not* include the withdrawal operation that might result
- * from this.
- */
-export interface PendingReserveTask {
- type: PendingTaskType.Reserve;
- retryInfo: RetryInfo | undefined;
- stage: ReserveRecordStatus;
- timestampCreated: TalerProtocolTimestamp;
- reserveType: ReserveType;
- reservePub: string;
- bankWithdrawConfirmUrl?: string;
-}
-
-/**
* Status of an ongoing withdrawal operation.
*/
export interface PendingRefreshTask {
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 066f91a30..a74c6c175 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -107,7 +107,6 @@ import {
MerchantOperations,
NotificationListener,
RecoupOperations,
- ReserveOperations,
} from "./internal-wallet-state.js";
import { exportBackup } from "./operations/backup/export.js";
import {
@@ -168,12 +167,6 @@ import {
processPurchaseQueryRefund,
} from "./operations/refund.js";
import {
- createReserve,
- createTalerWithdrawReserve,
- getFundingPaytoUris,
- processReserve,
-} from "./operations/reserves.js";
-import {
runIntegrationTest,
testPay,
withdrawTestBalance,
@@ -185,9 +178,12 @@ import {
retryTransaction,
} from "./operations/transactions.js";
import {
+ acceptWithdrawalFromUri,
+ createManualWithdrawal,
getExchangeWithdrawalInfo,
+ getFundingPaytoUrisTx,
getWithdrawalDetailsForUri,
- processWithdrawGroup,
+ processWithdrawalGroup as processWithdrawalGroup,
} from "./operations/withdraw.js";
import {
PendingOperationsResponse,
@@ -258,11 +254,8 @@ async function processOnePendingOperation(
case PendingTaskType.Refresh:
await processRefreshGroup(ws, pending.refreshGroupId, { forceNow });
break;
- case PendingTaskType.Reserve:
- await processReserve(ws, pending.reservePub, { forceNow });
- break;
case PendingTaskType.Withdraw:
- await processWithdrawGroup(ws, pending.withdrawalGroupId, { forceNow });
+ await processWithdrawalGroup(ws, pending.withdrawalGroupId, { forceNow });
break;
case PendingTaskType.ProposalDownload:
await processDownloadProposal(ws, pending.proposalId, { forceNow });
@@ -464,40 +457,6 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
});
}
-/**
- * Create a reserve for a manual withdrawal.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- */
-async function acceptManualWithdrawal(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- amount: AmountJson,
- restrictAge?: number,
-): Promise<AcceptManualWithdrawalResult> {
- try {
- const resp = await createReserve(ws, {
- amount,
- exchange: exchangeBaseUrl,
- restrictAge,
- });
- const exchangePaytoUris = await ws.db
- .mktx((x) => ({
- exchanges: x.exchanges,
- exchangeDetails: x.exchangeDetails,
- reserves: x.reserves,
- }))
- .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
- return {
- reservePub: resp.reservePub,
- exchangePaytoUris,
- };
- } finally {
- ws.latch.trigger();
- }
-}
-
async function getExchangeTos(
ws: InternalWalletState,
exchangeBaseUrl: string,
@@ -552,6 +511,10 @@ async function getExchangeTos(
};
}
+/**
+ * List bank accounts known to the wallet from
+ * previous withdrawals.
+ */
async function listKnownBankAccounts(
ws: InternalWalletState,
currency?: string,
@@ -559,12 +522,13 @@ async function listKnownBankAccounts(
const accounts: PaytoUri[] = [];
await ws.db
.mktx((x) => ({
- reserves: x.reserves,
+ withdrawalGroups: x.withdrawalGroups,
}))
.runReadOnly(async (tx) => {
- const reservesRecords = await tx.reserves.iter().toArray();
- for (const r of reservesRecords) {
- if (currency && currency !== r.currency) {
+ const withdrawalGroups = await tx.withdrawalGroups.iter().toArray();
+ for (const r of withdrawalGroups) {
+ const amount = r.rawWithdrawalAmount;
+ if (currency && currency !== amount.currency) {
continue;
}
const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined;
@@ -614,31 +578,6 @@ async function getExchanges(
return { exchanges };
}
-/**
- * Inform the wallet that the status of a reserve has changed (e.g. due to a
- * confirmation from the bank.).
- */
-export async function handleNotifyReserve(
- ws: InternalWalletState,
-): Promise<void> {
- const reserves = await ws.db
- .mktx((x) => ({
- reserves: x.reserves,
- }))
- .runReadOnly(async (tx) => {
- return tx.reserves.iter().toArray();
- });
- for (const r of reserves) {
- if (r.reserveStatus === ReserveRecordStatus.WaitConfirmBank) {
- try {
- processReserve(ws, r.reservePub);
- } catch (e) {
- console.error(e);
- }
- }
- }
-}
-
async function setCoinSuspended(
ws: InternalWalletState,
coinPub: string,
@@ -817,12 +756,11 @@ async function dispatchRequestInternal(
}
case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
- const res = await acceptManualWithdrawal(
- ws,
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
- req.restrictAge,
- );
+ const res = await createManualWithdrawal(ws, {
+ amount: Amounts.parseOrThrow(req.amount),
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ restrictAge: req.restrictAge,
+ });
return res;
}
case "getWithdrawalDetailsForAmount": {
@@ -856,15 +794,12 @@ async function dispatchRequestInternal(
case "acceptBankIntegratedWithdrawal": {
const req =
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
- return await createTalerWithdrawReserve(
- ws,
- req.talerWithdrawUri,
- req.exchangeBaseUrl,
- {
- forcedDenomSel: req.forcedDenomSel,
- restrictAge: req.restrictAge,
- },
- );
+ return await acceptWithdrawalFromUri(ws, {
+ selectedExchange: req.exchangeBaseUrl,
+ talerWithdrawUri: req.talerWithdrawUri,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ });
}
case "getExchangeTos": {
const req = codecForGetExchangeTosRequest().decode(payload);
@@ -1033,7 +968,10 @@ async function dispatchRequestInternal(
req.exchange,
amount,
);
- const wres = await acceptManualWithdrawal(ws, req.exchange, amount);
+ const wres = await createManualWithdrawal(ws, {
+ amount: amount,
+ exchangeBaseUrl: req.exchange,
+ });
const paytoUri = details.paytoUris[0];
const pt = parsePaytoUri(paytoUri);
if (!pt) {
@@ -1229,10 +1167,6 @@ class InternalWalletStateImpl implements InternalWalletState {
getMerchantInfo,
};
- reserveOps: ReserveOperations = {
- processReserve,
- };
-
// FIXME: Use an LRU cache here.
private denomCache: Record<string, DenomInfo> = {};