diff options
author | Florian Dold <florian@dold.me> | 2023-11-21 15:00:14 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-11-21 15:00:24 +0100 |
commit | 6000a55d583832a71335310514688f1f6faed722 (patch) | |
tree | c9b5ae2ca9f2511c75e3ed375ae5bbeade44ac66 | |
parent | 77eb29cb7caf50e870ada861a4ef83712a31a1cb (diff) | |
download | wallet-core-6000a55d583832a71335310514688f1f6faed722.tar.xz |
towards a currency conversion integration test
-rw-r--r-- | packages/taler-harness/src/harness/harness.ts | 6 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts | 247 | ||||
-rw-r--r-- | packages/taler-harness/src/integrationtests/testrunner.ts | 2 | ||||
-rw-r--r-- | packages/taler-util/src/merchant-api-types.ts | 8 | ||||
-rw-r--r-- | packages/taler-util/src/taler-types.ts | 48 | ||||
-rw-r--r-- | packages/taler-util/src/wallet-types.ts | 20 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 16 |
7 files changed, 282 insertions, 65 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 1253e3dd5..37e0b02a7 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -45,6 +45,7 @@ import { j2s, parsePaytoUri, stringToBytes, + AccountRestriction, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -589,6 +590,11 @@ export interface HarnessExchangeBankAccount { accountPassword: string; accountPaytoUri: string; wireGatewayApiBaseUrl: string; + + conversionUrl?: string; + + debitRestrictions?: AccountRestriction[]; + creditRestrictions?: AccountRestriction[]; } /** diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts new file mode 100644 index 000000000..2a9dd5800 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts @@ -0,0 +1,247 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + 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/> + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + AmountString, + Duration, + Logger, + TalerCorebankApiClient, + WireGatewayApiClient, + j2s, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + BankService, + ExchangeService, + GlobalTestState, + MerchantService, + generateRandomPayto, + setupDb, +} from "../harness/harness.js"; +import { createWalletDaemonWithClient } from "../harness/helpers.js"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import * as http from "node:http"; + +const logger = new Logger("test-withdrawal-conversion.ts"); + +interface TestfakeConversionService { + stop: () => void; +} + +function splitInTwoAt(s: string, separator: string): [string, string] { + const idx = s.indexOf(separator); + if (idx === -1) { + return [s, ""]; + } + return [s.slice(0, idx), s.slice(idx + 1)]; +} + +/** + * Testfake for the kyc service that the exchange talks to. + */ +async function runTestfakeConversionService(): Promise<TestfakeConversionService> { + const server = http.createServer((req, res) => { + const requestUrl = req.url!; + logger.info(`kyc: got ${req.method} request, ${requestUrl}`); + + const [path, query] = splitInTwoAt(requestUrl, "?"); + + const qp = new URLSearchParams(query); + + if (path === "/config") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + version: "0:0:0", + name: "taler-conversion-info", + regional_currency: {}, + fiat_currency: {}, + }), + ); + } else if (path === "/cashin-rate") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + amount_debit: "FOO:123", + amount_credit: "BAR:123", + }), + ); + } else { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ code: 1, message: "bad request" })); + } + }); + await new Promise<void>((resolve, reject) => { + server.listen(8071, () => resolve()); + }); + return { + stop() { + server.close(); + }, + }; +} + +/** + * Test for currency conversion during manual withdrawal. + */ +export async function runWithdrawalConversionTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "x", + ); + exchangeBankAccount.conversionUrl = "http://localhost:8071/"; + await exchange.addBankAccount("1", exchangeBankAccount); + + await bank.start(); + + await bank.pingUntilAvailable(); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [generateRandomPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [generateRandomPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { name: "wallet" }, + ); + + await runTestfakeConversionService(); + + // Create a withdrawal operation + + const bankAccessApiClient = new TalerCorebankApiClient( + bank.corebankApiBaseUrl, + ); + + const user = await bankAccessApiClient.createRandomBankUser(); + + await walletClient.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + }); + + const infoRes = walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + exchangeBaseUrl: exchange.baseUrl, + amount: "EXTCOIN:20" as AmountString, + }, + ); + + console.log(`withdrawal details: ${j2s(infoRes)}`); + + const tStart = AbsoluteTime.now(); + + logger.info("starting AcceptManualWithdrawal request"); + // We expect this to return immediately. + + const wres = await walletClient.call( + WalletApiOperation.AcceptManualWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + amount: "TESTKUDOS:10" as AmountString, + }, + ); + + logger.info("AcceptManualWithdrawal finished"); + logger.info(`result: ${j2s(wres)}`); + + // Check that the request did not go into long-polling. + const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now()); + if (typeof duration.d_ms !== "number" || duration.d_ms > 5 * 1000) { + throw Error("withdrawal took too long (longpolling issue)"); + } + + const reservePub: string = wres.reservePub; + + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: exchangeBankAccount.accountName, + password: exchangeBankAccount.accountPassword, + }, + }, + ); + + await wireGatewayApiClient.adminAddIncoming({ + amount: "TESTKUDOS:10", + debitAccountPayto: user.accountPaytoUri, + reservePub: reservePub, + }); + + await exchange.runWirewatchOnce(); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + // Check balance + + const balResp = await walletClient.call(WalletApiOperation.GetBalances, {}); + t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); +} + +runWithdrawalConversionTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 25bce5712..b363e58a9 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -95,6 +95,7 @@ import { runWalletGenDbTest } from "./test-wallet-gendb.js"; import { runLibeufinBankTest } from "./test-libeufin-bank.js"; import { runMultiExchangeTest } from "./test-multiexchange.js"; import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js"; +import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js"; /** * Test runner. @@ -173,6 +174,7 @@ const allTests: TestMainFunction[] = [ runWithdrawalBankIntegratedTest, runWithdrawalFakebankTest, runWithdrawalFeesTest, + runWithdrawalConversionTest, runWithdrawalHugeTest, runTermOfServiceFormatTest, runStoredBackupsTest, diff --git a/packages/taler-util/src/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts index 9a7740088..999246597 100644 --- a/packages/taler-util/src/merchant-api-types.ts +++ b/packages/taler-util/src/merchant-api-types.ts @@ -44,8 +44,8 @@ import { TalerProtocolDuration, codecForTimestamp, TalerProtocolTimestamp, - WireAccount, - codecForWireAccount, + ExchangeWireAccount, + codecForExchangeWireAccount, codecForList, FacadeCredentials, } from "@gnu-taler/taler-util"; @@ -376,13 +376,13 @@ export interface MerchantReserveCreateConfirmation { reserve_pub: EddsaPublicKeyString; // Wire accounts of the exchange where to transfer the funds. - accounts: WireAccount[]; + accounts: ExchangeWireAccount[]; } export const codecForMerchantReserveCreateConfirmation = (): Codec<MerchantReserveCreateConfirmation> => buildCodecForObject<MerchantReserveCreateConfirmation>() - .property("accounts", codecForList(codecForWireAccount())) + .property("accounts", codecForList(codecForExchangeWireAccount())) .property("reserve_pub", codecForString()) .build("MerchantReserveCreateConfirmation"); diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index c0c8cc17d..5774f09f7 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -761,7 +761,7 @@ export class ExchangeKeysJson { global_fees: GlobalFees[]; - accounts: AccountInfo[]; + accounts: ExchangeWireAccount[]; wire_fees: { [methodName: string]: WireFeesJson[] }; @@ -939,20 +939,6 @@ export class WireFeesJson { end_date: TalerProtocolTimestamp; } -export interface AccountInfo { - payto_uri: string; - master_sig: string; - // Will become mandatory in later protocol versions - conversion_url?: string; - credit_restrictions?: any; - debit_restrictions?: any; -} - -/** - * @deprecated - */ -export interface ExchangeWireJson { } - /** * Proposal returned from the contract URL. */ @@ -1516,7 +1502,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> => .property("version", codecForString()) .property("reserve_closing_delay", codecForDuration) .property("global_fees", codecForList(codecForGlobalFees())) - .property("accounts", codecForList(codecForAccountInfo())) + .property("accounts", codecForList(codecForExchangeWireAccount())) .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson()))) .property("denominations", codecForList(codecForNgDenominations)) .build("ExchangeKeysJson"); @@ -1530,15 +1516,6 @@ export const codecForWireFeesJson = (): Codec<WireFeesJson> => .property("end_date", codecForTimestamp) .build("WireFeesJson"); -export const codecForAccountInfo = (): Codec<AccountInfo> => - buildCodecForObject<AccountInfo>() - .property("payto_uri", codecForString()) - .property("master_sig", codecForString()) - .property("conversion_url", codecOptional(codecForString())) - .property("credit_restrictions", codecForAny()) - .property("debit_restrictions", codecForAny()) - .build("AccountInfo"); - export const codecForProposal = (): Codec<Proposal> => buildCodecForObject<Proposal>() .property("contract_terms", codecForAny()) @@ -1568,13 +1545,14 @@ export const codecForWithdrawOperationStatusResponse = .property("wire_types", codecForList(codecForString())) .build("WithdrawOperationStatusResponse"); -export const codecForRewardPickupGetResponse = (): Codec<RewardPickupGetResponse> => - buildCodecForObject<RewardPickupGetResponse>() - .property("reward_amount", codecForString()) - .property("exchange_url", codecForString()) - .property("next_url", codecOptional(codecForString())) - .property("expiration", codecForTimestamp) - .build("TipPickupGetResponse"); +export const codecForRewardPickupGetResponse = + (): Codec<RewardPickupGetResponse> => + buildCodecForObject<RewardPickupGetResponse>() + .property("reward_amount", codecForString()) + .property("exchange_url", codecForString()) + .property("next_url", codecOptional(codecForString())) + .property("expiration", codecForTimestamp) + .build("TipPickupGetResponse"); export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> => buildCodecForObject<RecoupConfirmation>() @@ -2376,7 +2354,7 @@ export interface RegexAccountRestriction { human_hint_i18n?: InternationalizedString; } -export interface WireAccount { +export interface ExchangeWireAccount { // payto:// URI identifying the account and wire method payto_uri: string; @@ -2401,8 +2379,8 @@ export interface WireAccount { master_sig: EddsaSignatureString; } -export const codecForWireAccount = (): Codec<WireAccount> => - buildCodecForObject<WireAccount>() +export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> => + buildCodecForObject<ExchangeWireAccount>() .property("conversion_url", codecOptional(codecForString())) .property("credit_restrictions", codecForList(codecForAny())) .property("debit_restrictions", codecForList(codecForAny())) diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 7a4ad91e8..148117673 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -58,11 +58,13 @@ import { DenomKeyType, DenominationPubKey, ExchangeAuditor, + ExchangeWireAccount, InternationalizedString, MerchantContractTerms, MerchantInfo, PeerContractTerms, UnblindedSignature, + codecForExchangeWireAccount, codecForMerchantContractTerms, codecForPeerContractTerms, } from "./taler-types.js"; @@ -1123,19 +1125,11 @@ export interface WireFee { sig: string; } -/** - * Information about one of the exchange's bank accounts. - */ -export interface ExchangeAccount { - payto_uri: string; - master_sig: string; -} - export type WireFeeMap = { [wireMethod: string]: WireFee[] }; export interface WireInfo { feesForType: WireFeeMap; - accounts: ExchangeAccount[]; + accounts: ExchangeWireAccount[]; } export interface ExchangeGlobalFees { @@ -1154,12 +1148,6 @@ export interface ExchangeGlobalFees { signature: string; } -const codecForExchangeAccount = (): Codec<ExchangeAccount> => - buildCodecForObject<ExchangeAccount>() - .property("payto_uri", codecForString()) - .property("master_sig", codecForString()) - .build("codecForExchangeAccount"); - const codecForWireFee = (): Codec<WireFee> => buildCodecForObject<WireFee>() .property("sig", codecForString()) @@ -1172,7 +1160,7 @@ const codecForWireFee = (): Codec<WireFee> => const codecForWireInfo = (): Codec<WireInfo> => buildCodecForObject<WireInfo>() .property("feesForType", codecForMap(codecForList(codecForWireFee()))) - .property("accounts", codecForList(codecForExchangeAccount())) + .property("accounts", codecForList(codecForExchangeWireAccount())) .build("codecForWireInfo"); export interface DenominationInfo { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 82d7b42bf..622f04bd3 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -19,23 +19,19 @@ */ import { AbsoluteTime, - AccountInfo, Amounts, CancellationToken, canonicalizeBaseUrl, codecForExchangeKeysJson, - DenomGroup, DenominationPubKey, DenomKeyType, Duration, durationFromSpec, encodeCrock, ExchangeAuditor, - ExchangeDenomination, - ExchangeEntryStatus, ExchangeGlobalFees, ExchangeSignKeyJson, - ExchangeWireJson, + ExchangeWireAccount, GlobalFees, hashDenomPub, j2s, @@ -58,10 +54,10 @@ import { WireInfo, } from "@gnu-taler/taler-util"; import { + getExpiry, HttpRequestLibrary, - readSuccessResponseTextOrThrow, readSuccessResponseJsonOrThrow, - getExpiry, + readSuccessResponseTextOrThrow, } from "@gnu-taler/taler-util/http"; import { DenominationRecord, @@ -79,7 +75,7 @@ import { timestampProtocolToDb, WalletDbReadWriteTransaction, } from "../index.js"; -import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { DbAccess, @@ -88,10 +84,10 @@ import { } from "../util/query.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js"; import { - TaskRunResultType, runTaskWithErrorReporting, TaskIdentifiers, TaskRunResult, + TaskRunResultType, } from "./common.js"; const logger = new Logger("exchanges.ts"); @@ -380,7 +376,7 @@ interface ExchangeKeysDownloadResult { recoup: Recoup[]; listIssueDate: TalerProtocolTimestamp; globalFees: GlobalFees[]; - accounts: AccountInfo[]; + accounts: ExchangeWireAccount[]; wireFees: { [methodName: string]: WireFeesJson[] }; } |