diff options
Diffstat (limited to 'packages/taler-wallet-core')
7 files changed, 468 insertions, 74 deletions
| diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts index 744c3b833..a61ea2eef 100644 --- a/packages/taler-wallet-core/src/bank-api-client.ts +++ b/packages/taler-wallet-core/src/bank-api-client.ts @@ -28,6 +28,8 @@ import {    codecForString,    encodeCrock,    getRandomBytes, +  j2s, +  Logger,  } from "@gnu-taler/taler-util";  import {    HttpRequestLibrary, @@ -35,6 +37,8 @@ import {    readSuccessResponseJsonOrThrow,  } from "./index.browser.js"; +const logger = new Logger("bank-api-client.ts"); +  export enum CreditDebitIndicator {    Credit = "credit",    Debit = "debit", @@ -98,6 +102,7 @@ export namespace BankApi {      const resp = await bank.http.postJson(url.href, { username, password });      let paytoUri = `payto://x-taler-bank/localhost/${username}`;      if (resp.status !== 200 && resp.status !== 202) { +      logger.error(`${j2s(await resp.json())}`)        throw new Error();      }      try { diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 00a7fba81..3b3396046 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -42,6 +42,7 @@ export interface RefreshNewDenomInfo {    value: AmountJson;    feeWithdraw: AmountJson;    denomPub: DenominationPubKey; +  denomPubHash: string;  }  /** diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts index b5a5950b1..820397346 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts @@ -30,6 +30,7 @@ import {    BlindedDenominationSignature,    CoinDepositPermission,    CoinEnvelope, +  PlanchetUnblindInfo,    RecoupRefreshRequest,    RecoupRequest,    UnblindedSignature, @@ -206,7 +207,7 @@ export class CryptoApi {        }      };      ws.terminationTimerHandle = timer.after(15 * 1000, destroy); -    //ws.terminationTimerHandle.unref(); +    ws.terminationTimerHandle.unref();    }    handleWorkerError(ws: WorkerState, e: any): void { @@ -331,7 +332,7 @@ export class CryptoApi {    }    unblindDenominationSignature(req: { -    planchet: WithdrawalPlanchet; +    planchet: PlanchetUnblindInfo;      evSig: BlindedDenominationSignature;    }): Promise<UnblindedSignature> {      return this.doRpc<UnblindedSignature>( diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts index 15a086ae1..b51d499d5 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts @@ -73,6 +73,7 @@ import {    BlindedDenominationSignature,    RsaUnblindedSignature,    UnblindedSignature, +  PlanchetUnblindInfo,  } from "@gnu-taler/taler-util";  import bigint from "big-integer";  import { DenominationRecord, WireFee } from "../../db.js"; @@ -432,7 +433,7 @@ export class CryptoImplementation {    }    unblindDenominationSignature(req: { -    planchet: WithdrawalPlanchet; +    planchet: PlanchetUnblindInfo;      evSig: BlindedDenominationSignature;    }): UnblindedSignature {      if (req.evSig.cipher === DenomKeyType.Rsa) { diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts new file mode 100644 index 000000000..85a72e28d --- /dev/null +++ b/packages/taler-wallet-core/src/dbless.ts @@ -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()); +} diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index cc01e914e..93430732a 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -54,4 +54,7 @@ export * from "./bank-api-client.js";  export * from "./operations/reserves.js";  export * from "./operations/withdraw.js"; +export * from "./operations/refresh.js"; + +export * from "./dbless.js"; diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index cc2a1c566..8b6d8b2e4 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -15,6 +15,7 @@   */  import { +  CoinPublicKeyString,    DenomKeyType,    encodeCrock,    ExchangeMeltRequest, @@ -79,8 +80,12 @@ import {    isWithdrawableDenom,    selectWithdrawalDenominations,  } from "./withdraw.js"; -import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js"; +import { +  DerivedRefreshSession, +  RefreshNewDenomInfo, +} from "../crypto/cryptoTypes.js";  import { GetReadWriteAccess } from "../util/query.js"; +import { CryptoApi } from "../index.browser.js";  const logger = new Logger("refresh.ts"); @@ -357,6 +362,7 @@ async function refreshMelt(          newCoinDenoms.push({            count: dh.count,            denomPub: newDenom.denomPub, +          denomPubHash: newDenom.denomPubHash,            feeWithdraw: newDenom.feeWithdraw,            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(    ws: InternalWalletState,    refreshGroupId: string, @@ -527,6 +589,7 @@ async function refreshReveal(          newCoinDenoms.push({            count: dh.count,            denomPub: newDenom.denomPub, +          denomPubHash: newDenom.denomPubHash,            feeWithdraw: newDenom.feeWithdraw,            value: newDenom.value,          }); @@ -575,46 +638,20 @@ async function refreshReveal(      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(      `refreshes/${derived.hash}/reveal`,      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 () => {      return await ws.http.postJson(reqUrl.href, req, {        timeout: getRefreshRequestTimeout(refreshGroup), @@ -629,51 +666,28 @@ async function refreshReveal(    const coins: CoinRecord[] = [];    for (let i = 0; i < refreshSession.newDenoms.length; i++) { +    const ncd = newCoinDenoms[i];      for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {        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]; -      if (denom.denomPub.cipher !== DenomKeyType.Rsa) { +      if (ncd.denomPub.cipher !== DenomKeyType.Rsa) {          throw Error("cipher unsupported");        }        const evSig = reveal.ev_sigs[newCoinIndex].ev_sig; -      let rsaSig: string; -      if (typeof evSig === "string") { -        rsaSig = evSig; -      } else if (evSig.cipher === DenomKeyType.Rsa) { -        rsaSig = evSig.blinded_rsa_signature; -      } else { -        throw Error("unsupported cipher"); -      } -      const denomSigRsa = await ws.cryptoApi.rsaUnblind( -        rsaSig, -        pc.blindingKey, -        denom.denomPub.rsa_public_key, -      ); +      const denomSig = await ws.cryptoApi.unblindDenominationSignature({ +        planchet: { +          blindingKey: pc.blindingKey, +          denomPub: ncd.denomPub, +        }, +        evSig, +      });        const coin: CoinRecord = {          blindingKey: pc.blindingKey,          coinPriv: pc.coinPriv,          coinPub: pc.coinPub, -        currentAmount: denom.value, -        denomPubHash: denom.denomPubHash, -        denomSig: { -          cipher: DenomKeyType.Rsa, -          rsa_signature: denomSigRsa, -        }, +        currentAmount: ncd.value, +        denomPubHash: ncd.denomPubHash, +        denomSig,          exchangeBaseUrl: oldCoin.exchangeBaseUrl,          status: CoinStatus.Fresh,          coinSource: { | 
