wallet: db-less benchmarking
This commit is contained in:
parent
eb18c1f179
commit
c0be242292
@ -458,6 +458,16 @@ export interface TalerErrorDetails {
|
|||||||
details: unknown;
|
details: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal information needed about a planchet for unblinding a signature.
|
||||||
|
*
|
||||||
|
* Can be a withdrawal/tipping/refresh planchet.
|
||||||
|
*/
|
||||||
|
export interface PlanchetUnblindInfo {
|
||||||
|
denomPub: DenominationPubKey;
|
||||||
|
blindingKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WithdrawalPlanchet {
|
export interface WithdrawalPlanchet {
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
coinPriv: string;
|
coinPriv: string;
|
||||||
|
@ -22,16 +22,20 @@ import {
|
|||||||
codecForNumber,
|
codecForNumber,
|
||||||
codecForString,
|
codecForString,
|
||||||
codecOptional,
|
codecOptional,
|
||||||
j2s,
|
|
||||||
Logger,
|
Logger,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
getDefaultNodeWallet2,
|
checkReserve,
|
||||||
NodeHttpLib,
|
createFakebankReserve,
|
||||||
WalletApiOperation,
|
CryptoApi,
|
||||||
Wallet,
|
depositCoin,
|
||||||
AccessStats,
|
|
||||||
downloadExchangeInfo,
|
downloadExchangeInfo,
|
||||||
|
findDenomOrThrow,
|
||||||
|
generateReserveKeypair,
|
||||||
|
NodeHttpLib,
|
||||||
|
refreshCoin,
|
||||||
|
SynchronousCryptoWorkerFactory,
|
||||||
|
withdrawCoin,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,15 +48,79 @@ export async function runBench2(configJson: any): Promise<void> {
|
|||||||
const logger = new Logger("Bench1");
|
const logger = new Logger("Bench1");
|
||||||
|
|
||||||
// Validate the configuration file for this benchmark.
|
// Validate the configuration file for this benchmark.
|
||||||
const benchConf = codecForBench1Config().decode(configJson);
|
const benchConf = codecForBench2Config().decode(configJson);
|
||||||
|
const curr = benchConf.currency;
|
||||||
|
const cryptoApi = new CryptoApi(new SynchronousCryptoWorkerFactory());
|
||||||
|
|
||||||
const myHttpLib = new NodeHttpLib();
|
const http = new NodeHttpLib();
|
||||||
myHttpLib.setThrottling(false);
|
http.setThrottling(false);
|
||||||
|
|
||||||
const exchangeInfo = await downloadExchangeInfo(
|
const numIter = benchConf.iterations ?? 1;
|
||||||
benchConf.exchange,
|
const numDeposits = benchConf.deposits ?? 5;
|
||||||
myHttpLib,
|
|
||||||
);
|
const reserveAmount = (numDeposits + 1) * 10;
|
||||||
|
|
||||||
|
for (let i = 0; i < numIter; i++) {
|
||||||
|
const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http);
|
||||||
|
|
||||||
|
const reserveKeyPair = generateReserveKeypair();
|
||||||
|
|
||||||
|
console.log("creating fakebank reserve");
|
||||||
|
|
||||||
|
await createFakebankReserve({
|
||||||
|
amount: `${curr}:${reserveAmount}`,
|
||||||
|
exchangeInfo,
|
||||||
|
fakebankBaseUrl: benchConf.bank,
|
||||||
|
http,
|
||||||
|
reservePub: reserveKeyPair.reservePub,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("waiting for reserve");
|
||||||
|
|
||||||
|
await checkReserve(http, benchConf.exchange, reserveKeyPair.reservePub);
|
||||||
|
|
||||||
|
console.log("reserve found");
|
||||||
|
|
||||||
|
const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8`);
|
||||||
|
|
||||||
|
for (let j = 0; j < numDeposits; j++) {
|
||||||
|
console.log("withdrawing coin");
|
||||||
|
const coin = await withdrawCoin({
|
||||||
|
http,
|
||||||
|
cryptoApi,
|
||||||
|
reserveKeyPair,
|
||||||
|
denom: d1,
|
||||||
|
exchangeBaseUrl: benchConf.exchange,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("depositing coin");
|
||||||
|
|
||||||
|
await depositCoin({
|
||||||
|
amount: `${curr}:4`,
|
||||||
|
coin: coin,
|
||||||
|
cryptoApi,
|
||||||
|
exchangeBaseUrl: benchConf.exchange,
|
||||||
|
http,
|
||||||
|
depositPayto: benchConf.payto,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshDenoms = [
|
||||||
|
findDenomOrThrow(exchangeInfo, `${curr}:1`),
|
||||||
|
findDenomOrThrow(exchangeInfo, `${curr}:1`),
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("refreshing coin");
|
||||||
|
|
||||||
|
await refreshCoin({
|
||||||
|
oldCoin: coin,
|
||||||
|
cryptoApi,
|
||||||
|
http,
|
||||||
|
newDenoms: refreshDenoms,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("refresh done");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,18 +151,12 @@ interface Bench2Config {
|
|||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
deposits?: number;
|
deposits?: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* How any iterations run until the wallet db gets purged
|
|
||||||
* Defaults to 20.
|
|
||||||
*/
|
|
||||||
restartAfter?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema validation codec for Bench1Config.
|
* Schema validation codec for Bench1Config.
|
||||||
*/
|
*/
|
||||||
const codecForBench1Config = () =>
|
const codecForBench2Config = () =>
|
||||||
buildCodecForObject<Bench2Config>()
|
buildCodecForObject<Bench2Config>()
|
||||||
.property("bank", codecForString())
|
.property("bank", codecForString())
|
||||||
.property("payto", codecForString())
|
.property("payto", codecForString())
|
||||||
@ -102,5 +164,4 @@ const codecForBench1Config = () =>
|
|||||||
.property("iterations", codecOptional(codecForNumber()))
|
.property("iterations", codecOptional(codecForNumber()))
|
||||||
.property("deposits", codecOptional(codecForNumber()))
|
.property("deposits", codecOptional(codecForNumber()))
|
||||||
.property("currency", codecForString())
|
.property("currency", codecForString())
|
||||||
.property("restartAfter", codecOptional(codecForNumber()))
|
.build("Bench2Config");
|
||||||
.build("Bench1Config");
|
|
||||||
|
@ -62,6 +62,7 @@ import { lintExchangeDeployment } from "./lint.js";
|
|||||||
import { runBench1 } from "./bench1.js";
|
import { runBench1 } from "./bench1.js";
|
||||||
import { runEnv1 } from "./env1.js";
|
import { runEnv1 } from "./env1.js";
|
||||||
import { GlobalTestState, runTestWithState } from "./harness/harness.js";
|
import { GlobalTestState, runTestWithState } from "./harness/harness.js";
|
||||||
|
import { runBench2 } from "./bench2.js";
|
||||||
|
|
||||||
// This module also serves as the entry point for the crypto
|
// This module also serves as the entry point for the crypto
|
||||||
// thread worker, and thus must expose these two handlers.
|
// thread worker, and thus must expose these two handlers.
|
||||||
@ -168,8 +169,7 @@ export const walletCli = clk
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.maybeOption("inhibit", ["--inhibit"], clk.STRING, {
|
.maybeOption("inhibit", ["--inhibit"], clk.STRING, {
|
||||||
help:
|
help: "Inhibit running certain operations, useful for debugging and testing.",
|
||||||
"Inhibit running certain operations, useful for debugging and testing.",
|
|
||||||
})
|
})
|
||||||
.flag("noThrottle", ["--no-throttle"], {
|
.flag("noThrottle", ["--no-throttle"], {
|
||||||
help: "Don't do any request throttling.",
|
help: "Don't do any request throttling.",
|
||||||
@ -559,8 +559,7 @@ backupCli.subcommand("status", "status").action(async (args) => {
|
|||||||
backupCli
|
backupCli
|
||||||
.subcommand("recoveryLoad", "load-recovery")
|
.subcommand("recoveryLoad", "load-recovery")
|
||||||
.maybeOption("strategy", ["--strategy"], clk.STRING, {
|
.maybeOption("strategy", ["--strategy"], clk.STRING, {
|
||||||
help:
|
help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')",
|
||||||
"Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')",
|
|
||||||
})
|
})
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
await withWallet(args, async (wallet) => {
|
await withWallet(args, async (wallet) => {
|
||||||
@ -636,8 +635,7 @@ depositCli
|
|||||||
});
|
});
|
||||||
|
|
||||||
const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
||||||
help:
|
help: "Subcommands for advanced operations (only use if you know what you're doing!).",
|
||||||
"Subcommands for advanced operations (only use if you know what you're doing!).",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
advancedCli
|
advancedCli
|
||||||
@ -655,6 +653,21 @@ advancedCli
|
|||||||
await runBench1(config);
|
await runBench1(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
advancedCli
|
||||||
|
.subcommand("bench2", "bench2", {
|
||||||
|
help: "Run the 'bench2' benchmark",
|
||||||
|
})
|
||||||
|
.requiredOption("configJson", ["--config-json"], clk.STRING)
|
||||||
|
.action(async (args) => {
|
||||||
|
let config: any;
|
||||||
|
try {
|
||||||
|
config = JSON.parse(args.bench2.configJson);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not parse config JSON");
|
||||||
|
}
|
||||||
|
await runBench2(config);
|
||||||
|
});
|
||||||
|
|
||||||
advancedCli
|
advancedCli
|
||||||
.subcommand("env1", "env1", {
|
.subcommand("env1", "env1", {
|
||||||
help: "Run a test environment for bench1",
|
help: "Run a test environment for bench1",
|
||||||
|
@ -17,277 +17,24 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
|
import { j2s } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AmountJson,
|
checkReserve,
|
||||||
AmountLike,
|
|
||||||
Amounts,
|
|
||||||
AmountString,
|
|
||||||
codecForBankWithdrawalOperationPostResponse,
|
|
||||||
codecForDepositSuccess,
|
|
||||||
codecForExchangeMeltResponse,
|
|
||||||
codecForWithdrawResponse,
|
|
||||||
DenominationPubKey,
|
|
||||||
eddsaGetPublic,
|
|
||||||
encodeCrock,
|
|
||||||
ExchangeMeltRequest,
|
|
||||||
ExchangeProtocolVersion,
|
|
||||||
ExchangeWithdrawRequest,
|
|
||||||
getRandomBytes,
|
|
||||||
getTimestampNow,
|
|
||||||
hashWire,
|
|
||||||
j2s,
|
|
||||||
Timestamp,
|
|
||||||
UnblindedSignature,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import {
|
|
||||||
BankAccessApi,
|
|
||||||
BankApi,
|
|
||||||
BankServiceHandle,
|
|
||||||
CryptoApi,
|
CryptoApi,
|
||||||
DenominationRecord,
|
depositCoin,
|
||||||
downloadExchangeInfo,
|
downloadExchangeInfo,
|
||||||
ExchangeInfo,
|
findDenomOrThrow,
|
||||||
getBankWithdrawalInfo,
|
generateReserveKeypair,
|
||||||
HttpRequestLibrary,
|
|
||||||
isWithdrawableDenom,
|
|
||||||
NodeHttpLib,
|
NodeHttpLib,
|
||||||
OperationFailedError,
|
OperationFailedError,
|
||||||
readSuccessResponseJsonOrThrow,
|
refreshCoin,
|
||||||
SynchronousCryptoWorkerFactory,
|
SynchronousCryptoWorkerFactory,
|
||||||
|
topupReserveWithDemobank,
|
||||||
|
withdrawCoin,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import { GlobalTestState } from "../harness/harness.js";
|
import { GlobalTestState } from "../harness/harness.js";
|
||||||
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||||
|
|
||||||
const httpLib = new NodeHttpLib();
|
|
||||||
|
|
||||||
export interface ReserveKeypair {
|
|
||||||
reservePub: string;
|
|
||||||
reservePriv: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Denormalized info about a coin.
|
|
||||||
*/
|
|
||||||
export interface CoinInfo {
|
|
||||||
coinPub: string;
|
|
||||||
coinPriv: string;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
denomSig: UnblindedSignature;
|
|
||||||
denomPub: DenominationPubKey;
|
|
||||||
denomPubHash: string;
|
|
||||||
feeDeposit: string;
|
|
||||||
feeRefresh: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateReserveKeypair(): ReserveKeypair {
|
|
||||||
const priv = getRandomBytes(32);
|
|
||||||
const pub = eddsaGetPublic(priv);
|
|
||||||
return {
|
|
||||||
reservePriv: encodeCrock(priv),
|
|
||||||
reservePub: encodeCrock(pub),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function topupReserveWithDemobank(
|
|
||||||
reservePub: string,
|
|
||||||
bankBaseUrl: string,
|
|
||||||
exchangeInfo: ExchangeInfo,
|
|
||||||
amount: AmountString,
|
|
||||||
) {
|
|
||||||
const bankHandle: BankServiceHandle = {
|
|
||||||
baseUrl: bankBaseUrl,
|
|
||||||
http: httpLib,
|
|
||||||
};
|
|
||||||
const bankUser = await BankApi.createRandomBankUser(bankHandle);
|
|
||||||
const wopi = await BankAccessApi.createWithdrawalOperation(
|
|
||||||
bankHandle,
|
|
||||||
bankUser,
|
|
||||||
amount,
|
|
||||||
);
|
|
||||||
const bankInfo = await getBankWithdrawalInfo(
|
|
||||||
httpLib,
|
|
||||||
wopi.taler_withdraw_uri,
|
|
||||||
);
|
|
||||||
const bankStatusUrl = bankInfo.extractedStatusUrl;
|
|
||||||
if (!bankInfo.suggestedExchange) {
|
|
||||||
throw Error("no suggested exchange");
|
|
||||||
}
|
|
||||||
const plainPaytoUris =
|
|
||||||
exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? [];
|
|
||||||
if (plainPaytoUris.length <= 0) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
const httpResp = await httpLib.postJson(bankStatusUrl, {
|
|
||||||
reserve_pub: reservePub,
|
|
||||||
selected_exchange: plainPaytoUris[0],
|
|
||||||
});
|
|
||||||
await readSuccessResponseJsonOrThrow(
|
|
||||||
httpResp,
|
|
||||||
codecForBankWithdrawalOperationPostResponse(),
|
|
||||||
);
|
|
||||||
await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function withdrawCoin(args: {
|
|
||||||
http: HttpRequestLibrary;
|
|
||||||
cryptoApi: CryptoApi;
|
|
||||||
reserveKeyPair: ReserveKeypair;
|
|
||||||
denom: DenominationRecord;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
}): Promise<CoinInfo> {
|
|
||||||
const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args;
|
|
||||||
const planchet = await cryptoApi.createPlanchet({
|
|
||||||
coinIndex: 0,
|
|
||||||
denomPub: denom.denomPub,
|
|
||||||
feeWithdraw: denom.feeWithdraw,
|
|
||||||
reservePriv: reserveKeyPair.reservePriv,
|
|
||||||
reservePub: reserveKeyPair.reservePub,
|
|
||||||
secretSeed: encodeCrock(getRandomBytes(32)),
|
|
||||||
value: denom.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const reqBody: ExchangeWithdrawRequest = {
|
|
||||||
denom_pub_hash: planchet.denomPubHash,
|
|
||||||
reserve_sig: planchet.withdrawSig,
|
|
||||||
coin_ev: planchet.coinEv,
|
|
||||||
};
|
|
||||||
const reqUrl = new URL(
|
|
||||||
`reserves/${planchet.reservePub}/withdraw`,
|
|
||||||
exchangeBaseUrl,
|
|
||||||
).href;
|
|
||||||
|
|
||||||
const resp = await http.postJson(reqUrl, reqBody);
|
|
||||||
const r = await readSuccessResponseJsonOrThrow(
|
|
||||||
resp,
|
|
||||||
codecForWithdrawResponse(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const ubSig = await cryptoApi.unblindDenominationSignature({
|
|
||||||
planchet,
|
|
||||||
evSig: r.ev_sig,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
coinPriv: planchet.coinPriv,
|
|
||||||
coinPub: planchet.coinPub,
|
|
||||||
denomSig: ubSig,
|
|
||||||
denomPub: denom.denomPub,
|
|
||||||
denomPubHash: denom.denomPubHash,
|
|
||||||
feeDeposit: Amounts.stringify(denom.feeDeposit),
|
|
||||||
feeRefresh: Amounts.stringify(denom.feeRefresh),
|
|
||||||
exchangeBaseUrl: args.exchangeBaseUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function findDenomOrThrow(
|
|
||||||
exchangeInfo: ExchangeInfo,
|
|
||||||
amount: AmountString,
|
|
||||||
): DenominationRecord {
|
|
||||||
for (const d of exchangeInfo.keys.currentDenominations) {
|
|
||||||
if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("no matching denomination found");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function depositCoin(args: {
|
|
||||||
http: HttpRequestLibrary;
|
|
||||||
cryptoApi: CryptoApi;
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
coin: CoinInfo;
|
|
||||||
amount: AmountString;
|
|
||||||
}) {
|
|
||||||
const { coin, http, cryptoApi } = args;
|
|
||||||
const depositPayto = "payto://x-taler-bank/localhost/foo";
|
|
||||||
const wireSalt = encodeCrock(getRandomBytes(16));
|
|
||||||
const contractTermsHash = encodeCrock(getRandomBytes(64));
|
|
||||||
const depositTimestamp = getTimestampNow();
|
|
||||||
const refundDeadline = getTimestampNow();
|
|
||||||
const merchantPub = encodeCrock(getRandomBytes(32));
|
|
||||||
const dp = await cryptoApi.signDepositPermission({
|
|
||||||
coinPriv: coin.coinPriv,
|
|
||||||
coinPub: coin.coinPub,
|
|
||||||
contractTermsHash,
|
|
||||||
denomKeyType: coin.denomPub.cipher,
|
|
||||||
denomPubHash: coin.denomPubHash,
|
|
||||||
denomSig: coin.denomSig,
|
|
||||||
exchangeBaseUrl: args.exchangeBaseUrl,
|
|
||||||
feeDeposit: Amounts.parseOrThrow(coin.feeDeposit),
|
|
||||||
merchantPub,
|
|
||||||
spendAmount: Amounts.parseOrThrow(args.amount),
|
|
||||||
timestamp: depositTimestamp,
|
|
||||||
refundDeadline: refundDeadline,
|
|
||||||
wireInfoHash: hashWire(depositPayto, wireSalt),
|
|
||||||
});
|
|
||||||
const requestBody = {
|
|
||||||
contribution: Amounts.stringify(dp.contribution),
|
|
||||||
merchant_payto_uri: depositPayto,
|
|
||||||
wire_salt: wireSalt,
|
|
||||||
h_contract_terms: contractTermsHash,
|
|
||||||
ub_sig: coin.denomSig,
|
|
||||||
timestamp: depositTimestamp,
|
|
||||||
wire_transfer_deadline: getTimestampNow(),
|
|
||||||
refund_deadline: refundDeadline,
|
|
||||||
coin_sig: dp.coin_sig,
|
|
||||||
denom_pub_hash: dp.h_denom,
|
|
||||||
merchant_pub: merchantPub,
|
|
||||||
};
|
|
||||||
const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url);
|
|
||||||
const httpResp = await http.postJson(url.href, requestBody);
|
|
||||||
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshCoin(req: {
|
|
||||||
http: HttpRequestLibrary;
|
|
||||||
cryptoApi: CryptoApi;
|
|
||||||
oldCoin: CoinInfo;
|
|
||||||
newDenoms: DenominationRecord[];
|
|
||||||
}): Promise<void> {
|
|
||||||
const { cryptoApi, oldCoin, http } = req;
|
|
||||||
const refreshSessionSeed = encodeCrock(getRandomBytes(32));
|
|
||||||
const session = await cryptoApi.deriveRefreshSession({
|
|
||||||
exchangeProtocolVersion: ExchangeProtocolVersion.V12,
|
|
||||||
feeRefresh: Amounts.parseOrThrow(oldCoin.feeRefresh),
|
|
||||||
kappa: 3,
|
|
||||||
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
|
||||||
meltCoinPriv: oldCoin.coinPriv,
|
|
||||||
meltCoinPub: oldCoin.coinPub,
|
|
||||||
sessionSecretSeed: refreshSessionSeed,
|
|
||||||
newCoinDenoms: req.newDenoms.map((x) => ({
|
|
||||||
count: 1,
|
|
||||||
denomPub: x.denomPub,
|
|
||||||
feeWithdraw: x.feeWithdraw,
|
|
||||||
value: x.value,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const meltReqBody: ExchangeMeltRequest = {
|
|
||||||
coin_pub: oldCoin.coinPub,
|
|
||||||
confirm_sig: session.confirmSig,
|
|
||||||
denom_pub_hash: oldCoin.denomPubHash,
|
|
||||||
denom_sig: oldCoin.denomSig,
|
|
||||||
rc: session.hash,
|
|
||||||
value_with_fee: Amounts.stringify(session.meltValueWithFee),
|
|
||||||
};
|
|
||||||
|
|
||||||
const reqUrl = new URL(
|
|
||||||
`coins/${oldCoin.coinPub}/melt`,
|
|
||||||
oldCoin.exchangeBaseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const resp = await http.postJson(reqUrl.href, meltReqBody);
|
|
||||||
|
|
||||||
const meltResponse = await readSuccessResponseJsonOrThrow(
|
|
||||||
resp,
|
|
||||||
codecForExchangeMeltResponse(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const norevealIndex = meltResponse.noreveal_index;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run test for basic, bank-integrated withdrawal and payment.
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
*/
|
*/
|
||||||
@ -307,6 +54,7 @@ export async function runWalletDblessTest(t: GlobalTestState) {
|
|||||||
const reserveKeyPair = generateReserveKeypair();
|
const reserveKeyPair = generateReserveKeypair();
|
||||||
|
|
||||||
await topupReserveWithDemobank(
|
await topupReserveWithDemobank(
|
||||||
|
http,
|
||||||
reserveKeyPair.reservePub,
|
reserveKeyPair.reservePub,
|
||||||
bank.baseUrl,
|
bank.baseUrl,
|
||||||
exchangeInfo,
|
exchangeInfo,
|
||||||
@ -315,6 +63,8 @@ export async function runWalletDblessTest(t: GlobalTestState) {
|
|||||||
|
|
||||||
await exchange.runWirewatchOnce();
|
await exchange.runWirewatchOnce();
|
||||||
|
|
||||||
|
await checkReserve(http, exchange.baseUrl, reserveKeyPair.reservePub);
|
||||||
|
|
||||||
const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8");
|
const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8");
|
||||||
|
|
||||||
const coin = await withdrawCoin({
|
const coin = await withdrawCoin({
|
||||||
@ -338,7 +88,7 @@ export async function runWalletDblessTest(t: GlobalTestState) {
|
|||||||
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
|
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const freshCoins = await refreshCoin({
|
await refreshCoin({
|
||||||
oldCoin: coin,
|
oldCoin: coin,
|
||||||
cryptoApi,
|
cryptoApi,
|
||||||
http,
|
http,
|
||||||
|
@ -28,6 +28,8 @@ import {
|
|||||||
codecForString,
|
codecForString,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
j2s,
|
||||||
|
Logger,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
@ -35,6 +37,8 @@ import {
|
|||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "./index.browser.js";
|
} from "./index.browser.js";
|
||||||
|
|
||||||
|
const logger = new Logger("bank-api-client.ts");
|
||||||
|
|
||||||
export enum CreditDebitIndicator {
|
export enum CreditDebitIndicator {
|
||||||
Credit = "credit",
|
Credit = "credit",
|
||||||
Debit = "debit",
|
Debit = "debit",
|
||||||
@ -98,6 +102,7 @@ export namespace BankApi {
|
|||||||
const resp = await bank.http.postJson(url.href, { username, password });
|
const resp = await bank.http.postJson(url.href, { username, password });
|
||||||
let paytoUri = `payto://x-taler-bank/localhost/${username}`;
|
let paytoUri = `payto://x-taler-bank/localhost/${username}`;
|
||||||
if (resp.status !== 200 && resp.status !== 202) {
|
if (resp.status !== 200 && resp.status !== 202) {
|
||||||
|
logger.error(`${j2s(await resp.json())}`)
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -42,6 +42,7 @@ export interface RefreshNewDenomInfo {
|
|||||||
value: AmountJson;
|
value: AmountJson;
|
||||||
feeWithdraw: AmountJson;
|
feeWithdraw: AmountJson;
|
||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
|
denomPubHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
|
PlanchetUnblindInfo,
|
||||||
RecoupRefreshRequest,
|
RecoupRefreshRequest,
|
||||||
RecoupRequest,
|
RecoupRequest,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
@ -206,7 +207,7 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
|
ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
|
||||||
//ws.terminationTimerHandle.unref();
|
ws.terminationTimerHandle.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWorkerError(ws: WorkerState, e: any): void {
|
handleWorkerError(ws: WorkerState, e: any): void {
|
||||||
@ -331,7 +332,7 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unblindDenominationSignature(req: {
|
unblindDenominationSignature(req: {
|
||||||
planchet: WithdrawalPlanchet;
|
planchet: PlanchetUnblindInfo;
|
||||||
evSig: BlindedDenominationSignature;
|
evSig: BlindedDenominationSignature;
|
||||||
}): Promise<UnblindedSignature> {
|
}): Promise<UnblindedSignature> {
|
||||||
return this.doRpc<UnblindedSignature>(
|
return this.doRpc<UnblindedSignature>(
|
||||||
|
@ -73,6 +73,7 @@ import {
|
|||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
RsaUnblindedSignature,
|
RsaUnblindedSignature,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
|
PlanchetUnblindInfo,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import bigint from "big-integer";
|
import bigint from "big-integer";
|
||||||
import { DenominationRecord, WireFee } from "../../db.js";
|
import { DenominationRecord, WireFee } from "../../db.js";
|
||||||
@ -432,7 +433,7 @@ export class CryptoImplementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unblindDenominationSignature(req: {
|
unblindDenominationSignature(req: {
|
||||||
planchet: WithdrawalPlanchet;
|
planchet: PlanchetUnblindInfo;
|
||||||
evSig: BlindedDenominationSignature;
|
evSig: BlindedDenominationSignature;
|
||||||
}): UnblindedSignature {
|
}): UnblindedSignature {
|
||||||
if (req.evSig.cipher === DenomKeyType.Rsa) {
|
if (req.evSig.cipher === DenomKeyType.Rsa) {
|
||||||
|
369
packages/taler-wallet-core/src/dbless.ts
Normal file
369
packages/taler-wallet-core/src/dbless.ts
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper functions to run wallet functionality (withdrawal, deposit, refresh)
|
||||||
|
* without a database or retry loop.
|
||||||
|
*
|
||||||
|
* Used for benchmarking, where we want to benchmark the exchange, but the
|
||||||
|
* normal wallet would be too sluggish.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Amounts,
|
||||||
|
AmountString,
|
||||||
|
codecForAny,
|
||||||
|
codecForBankWithdrawalOperationPostResponse,
|
||||||
|
codecForDepositSuccess,
|
||||||
|
codecForExchangeMeltResponse,
|
||||||
|
codecForExchangeRevealResponse,
|
||||||
|
codecForWithdrawResponse,
|
||||||
|
DenominationPubKey,
|
||||||
|
eddsaGetPublic,
|
||||||
|
encodeCrock,
|
||||||
|
ExchangeMeltRequest,
|
||||||
|
ExchangeProtocolVersion,
|
||||||
|
ExchangeWithdrawRequest,
|
||||||
|
getRandomBytes,
|
||||||
|
getTimestampNow,
|
||||||
|
hashWire,
|
||||||
|
Logger,
|
||||||
|
parsePaytoUri,
|
||||||
|
UnblindedSignature,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { DenominationRecord } from "./db.js";
|
||||||
|
import {
|
||||||
|
assembleRefreshRevealRequest,
|
||||||
|
CryptoApi,
|
||||||
|
ExchangeInfo,
|
||||||
|
getBankWithdrawalInfo,
|
||||||
|
HttpRequestLibrary,
|
||||||
|
isWithdrawableDenom,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
} from "./index.browser.js";
|
||||||
|
import { BankAccessApi, BankApi, BankServiceHandle } from "./index.js";
|
||||||
|
|
||||||
|
const logger = new Logger("dbless.ts");
|
||||||
|
|
||||||
|
export interface ReserveKeypair {
|
||||||
|
reservePub: string;
|
||||||
|
reservePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denormalized info about a coin.
|
||||||
|
*/
|
||||||
|
export interface CoinInfo {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
denomPub: DenominationPubKey;
|
||||||
|
denomPubHash: string;
|
||||||
|
feeDeposit: string;
|
||||||
|
feeRefresh: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateReserveKeypair(): ReserveKeypair {
|
||||||
|
const priv = getRandomBytes(32);
|
||||||
|
const pub = eddsaGetPublic(priv);
|
||||||
|
return {
|
||||||
|
reservePriv: encodeCrock(priv),
|
||||||
|
reservePub: encodeCrock(pub),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the status of a reserve, use long-polling to wait
|
||||||
|
* until the reserve actually has been created.
|
||||||
|
*/
|
||||||
|
export async function checkReserve(
|
||||||
|
http: HttpRequestLibrary,
|
||||||
|
exchangeBaseUrl: string,
|
||||||
|
reservePub: string,
|
||||||
|
longpollTimeoutMs: number = 500,
|
||||||
|
): Promise<void> {
|
||||||
|
const reqUrl = new URL(`reserves/${reservePub}`, exchangeBaseUrl);
|
||||||
|
if (longpollTimeoutMs) {
|
||||||
|
reqUrl.searchParams.set("timeout_ms", `${longpollTimeoutMs}`);
|
||||||
|
}
|
||||||
|
const resp = await http.get(reqUrl.href);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error("reserve not okay");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function topupReserveWithDemobank(
|
||||||
|
http: HttpRequestLibrary,
|
||||||
|
reservePub: string,
|
||||||
|
bankBaseUrl: string,
|
||||||
|
exchangeInfo: ExchangeInfo,
|
||||||
|
amount: AmountString,
|
||||||
|
) {
|
||||||
|
const bankHandle: BankServiceHandle = {
|
||||||
|
baseUrl: bankBaseUrl,
|
||||||
|
http,
|
||||||
|
};
|
||||||
|
const bankUser = await BankApi.createRandomBankUser(bankHandle);
|
||||||
|
const wopi = await BankAccessApi.createWithdrawalOperation(
|
||||||
|
bankHandle,
|
||||||
|
bankUser,
|
||||||
|
amount,
|
||||||
|
);
|
||||||
|
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
|
||||||
|
const bankStatusUrl = bankInfo.extractedStatusUrl;
|
||||||
|
if (!bankInfo.suggestedExchange) {
|
||||||
|
throw Error("no suggested exchange");
|
||||||
|
}
|
||||||
|
const plainPaytoUris =
|
||||||
|
exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? [];
|
||||||
|
if (plainPaytoUris.length <= 0) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
const httpResp = await http.postJson(bankStatusUrl, {
|
||||||
|
reserve_pub: reservePub,
|
||||||
|
selected_exchange: plainPaytoUris[0],
|
||||||
|
});
|
||||||
|
await readSuccessResponseJsonOrThrow(
|
||||||
|
httpResp,
|
||||||
|
codecForBankWithdrawalOperationPostResponse(),
|
||||||
|
);
|
||||||
|
await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withdrawCoin(args: {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
cryptoApi: CryptoApi;
|
||||||
|
reserveKeyPair: ReserveKeypair;
|
||||||
|
denom: DenominationRecord;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
}): Promise<CoinInfo> {
|
||||||
|
const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args;
|
||||||
|
const planchet = await cryptoApi.createPlanchet({
|
||||||
|
coinIndex: 0,
|
||||||
|
denomPub: denom.denomPub,
|
||||||
|
feeWithdraw: denom.feeWithdraw,
|
||||||
|
reservePriv: reserveKeyPair.reservePriv,
|
||||||
|
reservePub: reserveKeyPair.reservePub,
|
||||||
|
secretSeed: encodeCrock(getRandomBytes(32)),
|
||||||
|
value: denom.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reqBody: ExchangeWithdrawRequest = {
|
||||||
|
denom_pub_hash: planchet.denomPubHash,
|
||||||
|
reserve_sig: planchet.withdrawSig,
|
||||||
|
coin_ev: planchet.coinEv,
|
||||||
|
};
|
||||||
|
const reqUrl = new URL(
|
||||||
|
`reserves/${planchet.reservePub}/withdraw`,
|
||||||
|
exchangeBaseUrl,
|
||||||
|
).href;
|
||||||
|
|
||||||
|
const resp = await http.postJson(reqUrl, reqBody);
|
||||||
|
const r = await readSuccessResponseJsonOrThrow(
|
||||||
|
resp,
|
||||||
|
codecForWithdrawResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ubSig = await cryptoApi.unblindDenominationSignature({
|
||||||
|
planchet,
|
||||||
|
evSig: r.ev_sig,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
coinPriv: planchet.coinPriv,
|
||||||
|
coinPub: planchet.coinPub,
|
||||||
|
denomSig: ubSig,
|
||||||
|
denomPub: denom.denomPub,
|
||||||
|
denomPubHash: denom.denomPubHash,
|
||||||
|
feeDeposit: Amounts.stringify(denom.feeDeposit),
|
||||||
|
feeRefresh: Amounts.stringify(denom.feeRefresh),
|
||||||
|
exchangeBaseUrl: args.exchangeBaseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findDenomOrThrow(
|
||||||
|
exchangeInfo: ExchangeInfo,
|
||||||
|
amount: AmountString,
|
||||||
|
): DenominationRecord {
|
||||||
|
for (const d of exchangeInfo.keys.currentDenominations) {
|
||||||
|
if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("no matching denomination found");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function depositCoin(args: {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
cryptoApi: CryptoApi;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
coin: CoinInfo;
|
||||||
|
amount: AmountString;
|
||||||
|
depositPayto?: string;
|
||||||
|
}) {
|
||||||
|
const { coin, http, cryptoApi } = args;
|
||||||
|
const depositPayto =
|
||||||
|
args.depositPayto ?? "payto://x-taler-bank/localhost/foo";
|
||||||
|
const wireSalt = encodeCrock(getRandomBytes(16));
|
||||||
|
const contractTermsHash = encodeCrock(getRandomBytes(64));
|
||||||
|
const depositTimestamp = getTimestampNow();
|
||||||
|
const refundDeadline = getTimestampNow();
|
||||||
|
const merchantPub = encodeCrock(getRandomBytes(32));
|
||||||
|
const dp = await cryptoApi.signDepositPermission({
|
||||||
|
coinPriv: coin.coinPriv,
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
contractTermsHash,
|
||||||
|
denomKeyType: coin.denomPub.cipher,
|
||||||
|
denomPubHash: coin.denomPubHash,
|
||||||
|
denomSig: coin.denomSig,
|
||||||
|
exchangeBaseUrl: args.exchangeBaseUrl,
|
||||||
|
feeDeposit: Amounts.parseOrThrow(coin.feeDeposit),
|
||||||
|
merchantPub,
|
||||||
|
spendAmount: Amounts.parseOrThrow(args.amount),
|
||||||
|
timestamp: depositTimestamp,
|
||||||
|
refundDeadline: refundDeadline,
|
||||||
|
wireInfoHash: hashWire(depositPayto, wireSalt),
|
||||||
|
});
|
||||||
|
const requestBody = {
|
||||||
|
contribution: Amounts.stringify(dp.contribution),
|
||||||
|
merchant_payto_uri: depositPayto,
|
||||||
|
wire_salt: wireSalt,
|
||||||
|
h_contract_terms: contractTermsHash,
|
||||||
|
ub_sig: coin.denomSig,
|
||||||
|
timestamp: depositTimestamp,
|
||||||
|
wire_transfer_deadline: getTimestampNow(),
|
||||||
|
refund_deadline: refundDeadline,
|
||||||
|
coin_sig: dp.coin_sig,
|
||||||
|
denom_pub_hash: dp.h_denom,
|
||||||
|
merchant_pub: merchantPub,
|
||||||
|
};
|
||||||
|
const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url);
|
||||||
|
const httpResp = await http.postJson(url.href, requestBody);
|
||||||
|
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshCoin(req: {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
cryptoApi: CryptoApi;
|
||||||
|
oldCoin: CoinInfo;
|
||||||
|
newDenoms: DenominationRecord[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const { cryptoApi, oldCoin, http } = req;
|
||||||
|
const refreshSessionSeed = encodeCrock(getRandomBytes(32));
|
||||||
|
const session = await cryptoApi.deriveRefreshSession({
|
||||||
|
exchangeProtocolVersion: ExchangeProtocolVersion.V12,
|
||||||
|
feeRefresh: Amounts.parseOrThrow(oldCoin.feeRefresh),
|
||||||
|
kappa: 3,
|
||||||
|
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
||||||
|
meltCoinPriv: oldCoin.coinPriv,
|
||||||
|
meltCoinPub: oldCoin.coinPub,
|
||||||
|
sessionSecretSeed: refreshSessionSeed,
|
||||||
|
newCoinDenoms: req.newDenoms.map((x) => ({
|
||||||
|
count: 1,
|
||||||
|
denomPub: x.denomPub,
|
||||||
|
denomPubHash: x.denomPubHash,
|
||||||
|
feeWithdraw: x.feeWithdraw,
|
||||||
|
value: x.value,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const meltReqBody: ExchangeMeltRequest = {
|
||||||
|
coin_pub: oldCoin.coinPub,
|
||||||
|
confirm_sig: session.confirmSig,
|
||||||
|
denom_pub_hash: oldCoin.denomPubHash,
|
||||||
|
denom_sig: oldCoin.denomSig,
|
||||||
|
rc: session.hash,
|
||||||
|
value_with_fee: Amounts.stringify(session.meltValueWithFee),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info("requesting melt");
|
||||||
|
|
||||||
|
const meltReqUrl = new URL(
|
||||||
|
`coins/${oldCoin.coinPub}/melt`,
|
||||||
|
oldCoin.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("requesting melt done");
|
||||||
|
|
||||||
|
const meltHttpResp = await http.postJson(meltReqUrl.href, meltReqBody);
|
||||||
|
|
||||||
|
const meltResponse = await readSuccessResponseJsonOrThrow(
|
||||||
|
meltHttpResp,
|
||||||
|
codecForExchangeMeltResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const norevealIndex = meltResponse.noreveal_index;
|
||||||
|
|
||||||
|
const revealRequest = await assembleRefreshRevealRequest({
|
||||||
|
cryptoApi,
|
||||||
|
derived: session,
|
||||||
|
newDenoms: req.newDenoms.map((x) => ({
|
||||||
|
count: 1,
|
||||||
|
denomPubHash: x.denomPubHash,
|
||||||
|
})),
|
||||||
|
norevealIndex,
|
||||||
|
oldCoinPriv: oldCoin.coinPriv,
|
||||||
|
oldCoinPub: oldCoin.coinPub,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("requesting reveal");
|
||||||
|
const reqUrl = new URL(
|
||||||
|
`refreshes/${session.hash}/reveal`,
|
||||||
|
oldCoin.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const revealResp = await http.postJson(reqUrl.href, revealRequest);
|
||||||
|
|
||||||
|
logger.info("requesting reveal done");
|
||||||
|
|
||||||
|
const reveal = await readSuccessResponseJsonOrThrow(
|
||||||
|
revealResp,
|
||||||
|
codecForExchangeRevealResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We could unblind here, but we only use this function to
|
||||||
|
// benchmark the exchange.
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFakebankReserve(args: {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
fakebankBaseUrl: string;
|
||||||
|
amount: string;
|
||||||
|
reservePub: string;
|
||||||
|
exchangeInfo: ExchangeInfo;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { http, fakebankBaseUrl, amount, reservePub } = args;
|
||||||
|
const paytoUri = args.exchangeInfo.wire.accounts[0].payto_uri;
|
||||||
|
const pt = parsePaytoUri(paytoUri);
|
||||||
|
if (!pt) {
|
||||||
|
throw Error("failed to parse payto URI");
|
||||||
|
}
|
||||||
|
const components = pt.targetPath.split("/");
|
||||||
|
const creditorAcct = components[components.length - 1];
|
||||||
|
const fbReq = await http.postJson(
|
||||||
|
new URL(`${creditorAcct}/admin/add-incoming`, fakebankBaseUrl).href,
|
||||||
|
{
|
||||||
|
amount,
|
||||||
|
reserve_pub: reservePub,
|
||||||
|
debit_account: "payto://x-taler-bank/localhost/testdebtor",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
|
||||||
|
}
|
@ -54,4 +54,7 @@ export * from "./bank-api-client.js";
|
|||||||
|
|
||||||
export * from "./operations/reserves.js";
|
export * from "./operations/reserves.js";
|
||||||
export * from "./operations/withdraw.js";
|
export * from "./operations/withdraw.js";
|
||||||
|
export * from "./operations/refresh.js";
|
||||||
|
|
||||||
|
export * from "./dbless.js";
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CoinPublicKeyString,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
ExchangeMeltRequest,
|
ExchangeMeltRequest,
|
||||||
@ -79,8 +80,12 @@ import {
|
|||||||
isWithdrawableDenom,
|
isWithdrawableDenom,
|
||||||
selectWithdrawalDenominations,
|
selectWithdrawalDenominations,
|
||||||
} from "./withdraw.js";
|
} from "./withdraw.js";
|
||||||
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
|
import {
|
||||||
|
DerivedRefreshSession,
|
||||||
|
RefreshNewDenomInfo,
|
||||||
|
} from "../crypto/cryptoTypes.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
|
import { CryptoApi } from "../index.browser.js";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -357,6 +362,7 @@ async function refreshMelt(
|
|||||||
newCoinDenoms.push({
|
newCoinDenoms.push({
|
||||||
count: dh.count,
|
count: dh.count,
|
||||||
denomPub: newDenom.denomPub,
|
denomPub: newDenom.denomPub,
|
||||||
|
denomPubHash: newDenom.denomPubHash,
|
||||||
feeWithdraw: newDenom.feeWithdraw,
|
feeWithdraw: newDenom.feeWithdraw,
|
||||||
value: newDenom.value,
|
value: newDenom.value,
|
||||||
});
|
});
|
||||||
@ -472,6 +478,62 @@ async function refreshMelt(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function assembleRefreshRevealRequest(args: {
|
||||||
|
cryptoApi: CryptoApi;
|
||||||
|
derived: DerivedRefreshSession;
|
||||||
|
norevealIndex: number;
|
||||||
|
oldCoinPub: CoinPublicKeyString;
|
||||||
|
oldCoinPriv: string;
|
||||||
|
newDenoms: {
|
||||||
|
denomPubHash: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
}): Promise<ExchangeRefreshRevealRequest> {
|
||||||
|
const {
|
||||||
|
derived,
|
||||||
|
norevealIndex,
|
||||||
|
cryptoApi,
|
||||||
|
oldCoinPriv,
|
||||||
|
oldCoinPub,
|
||||||
|
newDenoms,
|
||||||
|
} = args;
|
||||||
|
const privs = Array.from(derived.transferPrivs);
|
||||||
|
privs.splice(norevealIndex, 1);
|
||||||
|
|
||||||
|
const planchets = derived.planchetsForGammas[norevealIndex];
|
||||||
|
if (!planchets) {
|
||||||
|
throw Error("refresh index error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDenomsFlat: string[] = [];
|
||||||
|
const linkSigs: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < newDenoms.length; i++) {
|
||||||
|
const dsel = newDenoms[i];
|
||||||
|
for (let j = 0; j < dsel.count; j++) {
|
||||||
|
const newCoinIndex = linkSigs.length;
|
||||||
|
const linkSig = await cryptoApi.signCoinLink(
|
||||||
|
oldCoinPriv,
|
||||||
|
dsel.denomPubHash,
|
||||||
|
oldCoinPub,
|
||||||
|
derived.transferPubs[norevealIndex],
|
||||||
|
planchets[newCoinIndex].coinEv,
|
||||||
|
);
|
||||||
|
linkSigs.push(linkSig);
|
||||||
|
newDenomsFlat.push(dsel.denomPubHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const req: ExchangeRefreshRevealRequest = {
|
||||||
|
coin_evs: planchets.map((x) => x.coinEv),
|
||||||
|
new_denoms_h: newDenomsFlat,
|
||||||
|
transfer_privs: privs,
|
||||||
|
transfer_pub: derived.transferPubs[norevealIndex],
|
||||||
|
link_sigs: linkSigs,
|
||||||
|
};
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshReveal(
|
async function refreshReveal(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
@ -527,6 +589,7 @@ async function refreshReveal(
|
|||||||
newCoinDenoms.push({
|
newCoinDenoms.push({
|
||||||
count: dh.count,
|
count: dh.count,
|
||||||
denomPub: newDenom.denomPub,
|
denomPub: newDenom.denomPub,
|
||||||
|
denomPubHash: newDenom.denomPubHash,
|
||||||
feeWithdraw: newDenom.feeWithdraw,
|
feeWithdraw: newDenom.feeWithdraw,
|
||||||
value: newDenom.value,
|
value: newDenom.value,
|
||||||
});
|
});
|
||||||
@ -575,46 +638,20 @@ async function refreshReveal(
|
|||||||
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const privs = Array.from(derived.transferPrivs);
|
|
||||||
privs.splice(norevealIndex, 1);
|
|
||||||
|
|
||||||
const planchets = derived.planchetsForGammas[norevealIndex];
|
|
||||||
if (!planchets) {
|
|
||||||
throw Error("refresh index error");
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDenomsFlat: string[] = [];
|
|
||||||
const linkSigs: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
|
||||||
const dsel = refreshSession.newDenoms[i];
|
|
||||||
for (let j = 0; j < dsel.count; j++) {
|
|
||||||
const newCoinIndex = linkSigs.length;
|
|
||||||
const linkSig = await ws.cryptoApi.signCoinLink(
|
|
||||||
oldCoin.coinPriv,
|
|
||||||
dsel.denomPubHash,
|
|
||||||
oldCoin.coinPub,
|
|
||||||
derived.transferPubs[norevealIndex],
|
|
||||||
planchets[newCoinIndex].coinEv,
|
|
||||||
);
|
|
||||||
linkSigs.push(linkSig);
|
|
||||||
newDenomsFlat.push(dsel.denomPubHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const req: ExchangeRefreshRevealRequest = {
|
|
||||||
coin_evs: planchets.map((x) => x.coinEv),
|
|
||||||
new_denoms_h: newDenomsFlat,
|
|
||||||
transfer_privs: privs,
|
|
||||||
transfer_pub: derived.transferPubs[norevealIndex],
|
|
||||||
link_sigs: linkSigs,
|
|
||||||
};
|
|
||||||
|
|
||||||
const reqUrl = new URL(
|
const reqUrl = new URL(
|
||||||
`refreshes/${derived.hash}/reveal`,
|
`refreshes/${derived.hash}/reveal`,
|
||||||
oldCoin.exchangeBaseUrl,
|
oldCoin.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const req = await assembleRefreshRevealRequest({
|
||||||
|
cryptoApi: ws.cryptoApi,
|
||||||
|
derived,
|
||||||
|
newDenoms: newCoinDenoms,
|
||||||
|
norevealIndex: norevealIndex,
|
||||||
|
oldCoinPriv: oldCoin.coinPriv,
|
||||||
|
oldCoinPub: oldCoin.coinPub,
|
||||||
|
});
|
||||||
|
|
||||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||||
return await ws.http.postJson(reqUrl.href, req, {
|
return await ws.http.postJson(reqUrl.href, req, {
|
||||||
timeout: getRefreshRequestTimeout(refreshGroup),
|
timeout: getRefreshRequestTimeout(refreshGroup),
|
||||||
@ -629,51 +666,28 @@ async function refreshReveal(
|
|||||||
const coins: CoinRecord[] = [];
|
const coins: CoinRecord[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
||||||
|
const ncd = newCoinDenoms[i];
|
||||||
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
||||||
const newCoinIndex = coins.length;
|
const newCoinIndex = coins.length;
|
||||||
// FIXME: Look up in earlier transaction!
|
|
||||||
const denom = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
denominations: x.denominations,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.denominations.get([
|
|
||||||
oldCoin.exchangeBaseUrl,
|
|
||||||
refreshSession.newDenoms[i].denomPubHash,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
if (!denom) {
|
|
||||||
console.error("denom not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
|
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
|
||||||
if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
|
if (ncd.denomPub.cipher !== DenomKeyType.Rsa) {
|
||||||
throw Error("cipher unsupported");
|
throw Error("cipher unsupported");
|
||||||
}
|
}
|
||||||
const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
|
const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
|
||||||
let rsaSig: string;
|
const denomSig = await ws.cryptoApi.unblindDenominationSignature({
|
||||||
if (typeof evSig === "string") {
|
planchet: {
|
||||||
rsaSig = evSig;
|
blindingKey: pc.blindingKey,
|
||||||
} else if (evSig.cipher === DenomKeyType.Rsa) {
|
denomPub: ncd.denomPub,
|
||||||
rsaSig = evSig.blinded_rsa_signature;
|
},
|
||||||
} else {
|
evSig,
|
||||||
throw Error("unsupported cipher");
|
});
|
||||||
}
|
|
||||||
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
|
|
||||||
rsaSig,
|
|
||||||
pc.blindingKey,
|
|
||||||
denom.denomPub.rsa_public_key,
|
|
||||||
);
|
|
||||||
const coin: CoinRecord = {
|
const coin: CoinRecord = {
|
||||||
blindingKey: pc.blindingKey,
|
blindingKey: pc.blindingKey,
|
||||||
coinPriv: pc.coinPriv,
|
coinPriv: pc.coinPriv,
|
||||||
coinPub: pc.coinPub,
|
coinPub: pc.coinPub,
|
||||||
currentAmount: denom.value,
|
currentAmount: ncd.value,
|
||||||
denomPubHash: denom.denomPubHash,
|
denomPubHash: ncd.denomPubHash,
|
||||||
denomSig: {
|
denomSig,
|
||||||
cipher: DenomKeyType.Rsa,
|
|
||||||
rsa_signature: denomSigRsa,
|
|
||||||
},
|
|
||||||
exchangeBaseUrl: oldCoin.exchangeBaseUrl,
|
exchangeBaseUrl: oldCoin.exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
coinSource: {
|
coinSource: {
|
||||||
|
Loading…
Reference in New Issue
Block a user