diff options
author | Florian Dold <florian@dold.me> | 2022-08-09 15:00:45 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-08-16 17:55:12 +0200 |
commit | ac8f116780a860c8f4acfdf5553bf90d76afe236 (patch) | |
tree | 38abecb5ad3a3660161909ee9ca229d4ce08eb4a | |
parent | fb8372dfbf27b7b4e8b2fe4f81aa2ba18bfcf638 (diff) |
implement peer to peer push payments
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> = {}; |