taler-harness: add exchange-purse test
This commit is contained in:
parent
b13bd85215
commit
55bdc161b5
@ -0,0 +1,224 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2023 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,
|
||||
ContractTermsUtil,
|
||||
decodeCrock,
|
||||
Duration,
|
||||
encodeCrock,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
j2s,
|
||||
PeerContractTerms,
|
||||
TalerError,
|
||||
TalerPreciseTimestamp,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
checkReserve,
|
||||
CryptoDispatcher,
|
||||
downloadExchangeInfo,
|
||||
EncryptContractRequest,
|
||||
findDenomOrThrow,
|
||||
SpendCoinDetails,
|
||||
SynchronousCryptoWorkerFactoryPlain,
|
||||
topupReserveWithDemobank,
|
||||
Wallet,
|
||||
withdrawCoin,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
|
||||
|
||||
/**
|
||||
* Test the exchange's purse API.
|
||||
*/
|
||||
export async function runExchangePurseTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
|
||||
|
||||
const http = harnessHttpLib;
|
||||
const cryptoDisp = new CryptoDispatcher(
|
||||
new SynchronousCryptoWorkerFactoryPlain(),
|
||||
);
|
||||
const cryptoApi = cryptoDisp.cryptoApi;
|
||||
|
||||
try {
|
||||
// Withdraw digital cash into the wallet.
|
||||
|
||||
const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
|
||||
|
||||
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
|
||||
|
||||
let reserveUrl = new URL(
|
||||
`reserves/${reserveKeyPair.pub}`,
|
||||
exchange.baseUrl,
|
||||
);
|
||||
reserveUrl.searchParams.set("timeout_ms", "30000");
|
||||
const longpollReq = http.fetch(reserveUrl.href, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
await topupReserveWithDemobank({
|
||||
amount: "TESTKUDOS:10",
|
||||
http,
|
||||
reservePub: reserveKeyPair.pub,
|
||||
bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
|
||||
exchangeInfo,
|
||||
});
|
||||
|
||||
console.log("waiting for longpoll request");
|
||||
const resp = await longpollReq;
|
||||
console.log(`got response, status ${resp.status}`);
|
||||
|
||||
console.log(exchangeInfo);
|
||||
|
||||
await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
|
||||
|
||||
const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8", {
|
||||
denomselAllowLate: Wallet.defaultConfig.testing.denomselAllowLate,
|
||||
});
|
||||
|
||||
const coin = await withdrawCoin({
|
||||
http,
|
||||
cryptoApi,
|
||||
reserveKeyPair: {
|
||||
reservePriv: reserveKeyPair.priv,
|
||||
reservePub: reserveKeyPair.pub,
|
||||
},
|
||||
denom: d1,
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
});
|
||||
|
||||
const amount = "TESTKUDOS:5";
|
||||
const purseFee = "TESTKUDOS:0";
|
||||
|
||||
const mergeTimestamp = TalerPreciseTimestamp.now();
|
||||
|
||||
const contractTerms: PeerContractTerms = {
|
||||
amount,
|
||||
summary: "Hello",
|
||||
purse_expiration: AbsoluteTime.toProtocolTimestamp(
|
||||
AbsoluteTime.addDuration(
|
||||
AbsoluteTime.now(),
|
||||
Duration.fromSpec({ minutes: 1 }),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const mergeReservePair = await cryptoApi.createEddsaKeypair({});
|
||||
const pursePair = await cryptoApi.createEddsaKeypair({});
|
||||
const mergePair = await cryptoApi.createEddsaKeypair({});
|
||||
const contractPair = await cryptoApi.createEddsaKeypair({});
|
||||
const contractEncNonce = encodeCrock(getRandomBytes(24));
|
||||
|
||||
const pursePub = pursePair.pub;
|
||||
|
||||
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
||||
|
||||
const purseSigResp = await cryptoApi.signPurseCreation({
|
||||
hContractTerms,
|
||||
mergePub: mergePair.pub,
|
||||
minAge: 0,
|
||||
purseAmount: amount,
|
||||
purseExpiration: contractTerms.purse_expiration,
|
||||
pursePriv: pursePair.priv,
|
||||
});
|
||||
|
||||
const coinSpend: SpendCoinDetails = {
|
||||
ageCommitmentProof: undefined,
|
||||
coinPriv: coin.coinPriv,
|
||||
coinPub: coin.coinPub,
|
||||
contribution: amount,
|
||||
denomPubHash: coin.denomPubHash,
|
||||
denomSig: coin.denomSig,
|
||||
};
|
||||
|
||||
const depositSigsResp = await cryptoApi.signPurseDeposits({
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
pursePub: pursePair.pub,
|
||||
coins: [coinSpend],
|
||||
});
|
||||
|
||||
const encryptContractRequest: EncryptContractRequest = {
|
||||
contractTerms: contractTerms,
|
||||
mergePriv: mergePair.priv,
|
||||
pursePriv: pursePair.priv,
|
||||
pursePub: pursePair.pub,
|
||||
contractPriv: contractPair.priv,
|
||||
contractPub: contractPair.pub,
|
||||
nonce: contractEncNonce,
|
||||
};
|
||||
|
||||
const econtractResp = await cryptoApi.encryptContractForMerge(
|
||||
encryptContractRequest,
|
||||
);
|
||||
|
||||
const econtractHash = encodeCrock(
|
||||
hash(decodeCrock(econtractResp.econtract.econtract)),
|
||||
);
|
||||
|
||||
const createPurseUrl = new URL(
|
||||
`purses/${pursePair.pub}/create`,
|
||||
exchange.baseUrl,
|
||||
);
|
||||
|
||||
const reqBody = {
|
||||
amount: amount,
|
||||
merge_pub: mergePair.pub,
|
||||
purse_sig: purseSigResp.sig,
|
||||
h_contract_terms: hContractTerms,
|
||||
purse_expiration: contractTerms.purse_expiration,
|
||||
deposits: depositSigsResp.deposits,
|
||||
min_age: 0,
|
||||
econtract: econtractResp.econtract,
|
||||
};
|
||||
|
||||
const httpResp = await http.fetch(createPurseUrl.href, {
|
||||
method: "POST",
|
||||
body: reqBody,
|
||||
});
|
||||
|
||||
const respBody = await httpResp.json();
|
||||
|
||||
console.log("status", httpResp.status);
|
||||
|
||||
console.log(j2s(respBody));
|
||||
|
||||
const mergeUrl = new URL(`purses/${pursePub}/merge`, exchange.baseUrl);
|
||||
mergeUrl.searchParams.set("timeout_ms", "300");
|
||||
const statusResp = await http.fetch(mergeUrl.href, {});
|
||||
|
||||
const statusRespBody = await statusResp.json();
|
||||
|
||||
console.log(j2s(statusRespBody));
|
||||
|
||||
t.assertTrue(statusRespBody.merge_timestamp === undefined);
|
||||
} catch (e) {
|
||||
if (e instanceof TalerError) {
|
||||
console.log(e);
|
||||
console.log(j2s(e.errorDetail));
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
runExchangePurseTest.suites = ["wallet"];
|
@ -14,7 +14,12 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { CancellationToken, Logger, minimatch, setGlobalLogLevelFromString } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
CancellationToken,
|
||||
Logger,
|
||||
minimatch,
|
||||
setGlobalLogLevelFromString,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
@ -105,6 +110,7 @@ import { runPeerRepairTest } from "./test-peer-repair.js";
|
||||
import { runPaymentShareTest } from "./test-payment-share.js";
|
||||
import { runSimplePaymentTest } from "./test-simple-payment.js";
|
||||
import { runTermOfServiceFormatTest } from "./test-tos-format.js";
|
||||
import { runExchangePurseTest } from "./test-exchange-purse.js";
|
||||
|
||||
/**
|
||||
* Test runner.
|
||||
@ -137,6 +143,7 @@ const allTests: TestMainFunction[] = [
|
||||
runFeeRegressionTest,
|
||||
runForcedSelectionTest,
|
||||
runKycTest,
|
||||
runExchangePurseTest,
|
||||
runExchangeDepositTest,
|
||||
runLibeufinAnastasisFacadeTest,
|
||||
runLibeufinApiBankaccountTest,
|
||||
|
@ -239,3 +239,25 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
|
||||
isKnown: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function talerPaytoFromExchangeReserve(
|
||||
exchangeBaseUrl: string,
|
||||
reservePub: string,
|
||||
): string {
|
||||
const url = new URL(exchangeBaseUrl);
|
||||
let proto: string;
|
||||
if (url.protocol === "http:") {
|
||||
proto = "taler-reserve-http";
|
||||
} else if (url.protocol === "https:") {
|
||||
proto = "taler-reserve";
|
||||
} else {
|
||||
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
|
||||
}
|
||||
|
||||
let path = url.pathname;
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + "/";
|
||||
}
|
||||
|
||||
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
|
||||
}
|
||||
|
@ -1004,7 +1004,7 @@ export enum TalerSignaturePurpose {
|
||||
SYNC_BACKUP_UPLOAD = 1450,
|
||||
}
|
||||
|
||||
export const enum WalletAccountMergeFlags {
|
||||
export enum WalletAccountMergeFlags {
|
||||
/**
|
||||
* Not a legal mode!
|
||||
*/
|
||||
@ -1281,7 +1281,8 @@ export namespace AgeRestriction {
|
||||
}
|
||||
|
||||
const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
|
||||
"CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG");
|
||||
"CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
|
||||
);
|
||||
|
||||
export async function restrictionCommitSeeded(
|
||||
ageMask: number,
|
||||
|
@ -51,12 +51,8 @@ export * from "./operations/refresh.js";
|
||||
|
||||
export * from "./dbless.js";
|
||||
|
||||
export {
|
||||
nativeCryptoR,
|
||||
nativeCrypto,
|
||||
nullCrypto,
|
||||
TalerCryptoInterface,
|
||||
} from "./crypto/cryptoImplementation.js";
|
||||
export * from "./crypto/cryptoTypes.js";
|
||||
export * from "./crypto/cryptoImplementation.js";
|
||||
|
||||
export * from "./util/timer.js";
|
||||
export * from "./util/denominations.js";
|
||||
|
@ -420,28 +420,6 @@ export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
|
||||
.property("merge_timestamp", codecOptional(codecForTimestamp))
|
||||
.build("ExchangePurseStatus");
|
||||
|
||||
export function talerPaytoFromExchangeReserve(
|
||||
exchangeBaseUrl: string,
|
||||
reservePub: string,
|
||||
): string {
|
||||
const url = new URL(exchangeBaseUrl);
|
||||
let proto: string;
|
||||
if (url.protocol === "http:") {
|
||||
proto = "taler-reserve-http";
|
||||
} else if (url.protocol === "https:") {
|
||||
proto = "taler-reserve";
|
||||
} else {
|
||||
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
|
||||
}
|
||||
|
||||
let path = url.pathname;
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + "/";
|
||||
}
|
||||
|
||||
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
|
||||
}
|
||||
|
||||
export async function getMergeReserveInfo(
|
||||
ws: InternalWalletState,
|
||||
req: {
|
||||
|
@ -45,6 +45,7 @@ import {
|
||||
j2s,
|
||||
makeErrorDetail,
|
||||
stringifyTalerUri,
|
||||
talerPaytoFromExchangeReserve,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
readSuccessResponseJsonOrErrorCode,
|
||||
@ -74,7 +75,6 @@ import {
|
||||
import {
|
||||
codecForExchangePurseStatus,
|
||||
getMergeReserveInfo,
|
||||
talerPaytoFromExchangeReserve,
|
||||
} from "./pay-peer-common.js";
|
||||
import {
|
||||
constructTransactionIdentifier,
|
||||
|
@ -47,6 +47,7 @@ import {
|
||||
j2s,
|
||||
makeErrorDetail,
|
||||
parsePayPushUri,
|
||||
talerPaytoFromExchangeReserve,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||
import {
|
||||
@ -71,7 +72,6 @@ import { updateExchangeFromUrl } from "./exchanges.js";
|
||||
import {
|
||||
codecForExchangePurseStatus,
|
||||
getMergeReserveInfo,
|
||||
talerPaytoFromExchangeReserve,
|
||||
} from "./pay-peer-common.js";
|
||||
import {
|
||||
TransitionInfo,
|
||||
|
Loading…
Reference in New Issue
Block a user