wallet: support both protocol versions
This commit is contained in:
parent
403de8170e
commit
5c4c25516d
@ -417,3 +417,26 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodecType<T> = T extends Codec<infer X> ? X : any;
|
||||||
|
|
||||||
|
export function codecForEither<T extends Array<Codec<unknown>>>(
|
||||||
|
...alts: [...T]
|
||||||
|
): Codec<CodecType<T[number]>> {
|
||||||
|
return {
|
||||||
|
decode(x: any, c?: Context): any {
|
||||||
|
for (const alt of alts) {
|
||||||
|
try {
|
||||||
|
return alt.decode(x, c);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new DecodingError(
|
||||||
|
`No alternative matched at at ${renderContext(c)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = codecForEither(codecForString(), codecForNumber());
|
||||||
|
@ -27,14 +27,15 @@ export interface VersionMatchResult {
|
|||||||
* Is the first version compatible with the second?
|
* Is the first version compatible with the second?
|
||||||
*/
|
*/
|
||||||
compatible: boolean;
|
compatible: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the first version older (-1), newser (+1) or
|
* Is the first version older (-1), newer (+1) or
|
||||||
* identical (0)?
|
* identical (0)?
|
||||||
*/
|
*/
|
||||||
currentCmp: number;
|
currentCmp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Version {
|
export interface Version {
|
||||||
current: number;
|
current: number;
|
||||||
revision: number;
|
revision: number;
|
||||||
age: number;
|
age: number;
|
||||||
@ -64,7 +65,7 @@ export namespace LibtoolVersion {
|
|||||||
return { compatible, currentCmp };
|
return { compatible, currentCmp };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVersion(v: string): Version | undefined {
|
export function parseVersion(v: string): Version | undefined {
|
||||||
const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
|
const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
|
||||||
if (rest.length !== 0) {
|
if (rest.length !== 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -55,7 +55,7 @@ export function setGlobalLogLevelFromString(logLevelStr: string) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
process.stderr.write(`Invalid log level, defaulting to WARNING`);
|
process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Invalid log level, defaulting to WARNING`);
|
console.warn(`Invalid log level, defaulting to WARNING`);
|
||||||
}
|
}
|
||||||
@ -143,6 +143,7 @@ export class Logger {
|
|||||||
case LogLevel.Info:
|
case LogLevel.Info:
|
||||||
case LogLevel.Warn:
|
case LogLevel.Warn:
|
||||||
case LogLevel.Error:
|
case LogLevel.Error:
|
||||||
|
return true;
|
||||||
case LogLevel.None:
|
case LogLevel.None:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -349,10 +349,12 @@ export function hash(d: Uint8Array): Uint8Array {
|
|||||||
return nacl.hash(d);
|
return nacl.hash(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a denomination public key according to the
|
||||||
|
* algorithm of exchange protocol v10.
|
||||||
|
*/
|
||||||
export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
||||||
if (pub.cipher !== DenomKeyType.Rsa) {
|
if (pub.cipher === DenomKeyType.Rsa) {
|
||||||
throw Error("unsupported cipher");
|
|
||||||
}
|
|
||||||
const pubBuf = decodeCrock(pub.rsa_public_key);
|
const pubBuf = decodeCrock(pub.rsa_public_key);
|
||||||
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
|
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
|
||||||
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
|
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
|
||||||
@ -361,6 +363,11 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
|||||||
dv.setUint32(4, pub.cipher);
|
dv.setUint32(4, pub.cipher);
|
||||||
uint8ArrayBuf.set(pubBuf, 8);
|
uint8ArrayBuf.set(pubBuf, 8);
|
||||||
return nacl.hash(uint8ArrayBuf);
|
return nacl.hash(uint8ArrayBuf);
|
||||||
|
} else if (pub.cipher === DenomKeyType.LegacyRsa) {
|
||||||
|
return hash(decodeCrock(pub.rsa_public_key));
|
||||||
|
} else {
|
||||||
|
throw Error(`unsupported cipher (${pub.cipher}), unable to hash`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
|
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
codecForConstNumber,
|
codecForConstNumber,
|
||||||
buildCodecForUnion,
|
buildCodecForUnion,
|
||||||
codecForConstString,
|
codecForConstString,
|
||||||
|
codecForEither,
|
||||||
} from "./codec.js";
|
} from "./codec.js";
|
||||||
import {
|
import {
|
||||||
Timestamp,
|
Timestamp,
|
||||||
@ -50,7 +51,7 @@ import { codecForAmountString } from "./amounts.js";
|
|||||||
/**
|
/**
|
||||||
* Denomination as found in the /keys response from the exchange.
|
* Denomination as found in the /keys response from the exchange.
|
||||||
*/
|
*/
|
||||||
export class Denomination {
|
export class ExchangeDenomination {
|
||||||
/**
|
/**
|
||||||
* Value of one coin of the denomination.
|
* Value of one coin of the denomination.
|
||||||
*/
|
*/
|
||||||
@ -58,8 +59,11 @@ export class Denomination {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Public signing key of the denomination.
|
* Public signing key of the denomination.
|
||||||
|
*
|
||||||
|
* The "string" alternative is for the old exchange protocol (v9) that
|
||||||
|
* only supports RSA keys.
|
||||||
*/
|
*/
|
||||||
denom_pub: DenominationPubKey;
|
denom_pub: DenominationPubKey | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for withdrawing.
|
* Fee for withdrawing.
|
||||||
@ -128,7 +132,7 @@ export class AuditorDenomSig {
|
|||||||
/**
|
/**
|
||||||
* Auditor information as given by the exchange in /keys.
|
* Auditor information as given by the exchange in /keys.
|
||||||
*/
|
*/
|
||||||
export class Auditor {
|
export class ExchangeAuditor {
|
||||||
/**
|
/**
|
||||||
* Auditor's public key.
|
* Auditor's public key.
|
||||||
*/
|
*/
|
||||||
@ -157,8 +161,10 @@ export interface RecoupRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature over the coin public key by the denomination.
|
* Signature over the coin public key by the denomination.
|
||||||
|
*
|
||||||
|
* The string variant is for the legacy exchange protocol.
|
||||||
*/
|
*/
|
||||||
denom_sig: UnblindedSignature;
|
denom_sig: UnblindedSignature | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coin public key of the coin we want to refund.
|
* Coin public key of the coin we want to refund.
|
||||||
@ -198,11 +204,20 @@ export interface RecoupConfirmation {
|
|||||||
old_coin_pub?: string;
|
old_coin_pub?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnblindedSignature {
|
export type UnblindedSignature =
|
||||||
|
| RsaUnblindedSignature
|
||||||
|
| LegacyRsaUnblindedSignature;
|
||||||
|
|
||||||
|
export interface RsaUnblindedSignature {
|
||||||
cipher: DenomKeyType.Rsa;
|
cipher: DenomKeyType.Rsa;
|
||||||
rsa_signature: string;
|
rsa_signature: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LegacyRsaUnblindedSignature {
|
||||||
|
cipher: DenomKeyType.LegacyRsa;
|
||||||
|
rsa_signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deposit permission for a single coin.
|
* Deposit permission for a single coin.
|
||||||
*/
|
*/
|
||||||
@ -211,18 +226,25 @@ export interface CoinDepositPermission {
|
|||||||
* Signature by the coin.
|
* Signature by the coin.
|
||||||
*/
|
*/
|
||||||
coin_sig: string;
|
coin_sig: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key of the coin being spend.
|
* Public key of the coin being spend.
|
||||||
*/
|
*/
|
||||||
coin_pub: string;
|
coin_pub: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature made by the denomination public key.
|
* Signature made by the denomination public key.
|
||||||
|
*
|
||||||
|
* The string variant is for legacy protocol support.
|
||||||
*/
|
*/
|
||||||
ub_sig: UnblindedSignature;
|
|
||||||
|
ub_sig: UnblindedSignature | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The denomination public key associated with this coin.
|
* The denomination public key associated with this coin.
|
||||||
*/
|
*/
|
||||||
h_denom: string;
|
h_denom: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount that is subtracted from this coin with this payment.
|
* The amount that is subtracted from this coin with this payment.
|
||||||
*/
|
*/
|
||||||
@ -358,6 +380,11 @@ export interface ContractTerms {
|
|||||||
*/
|
*/
|
||||||
h_wire: string;
|
h_wire: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy wire hash, used for deposit operations with an older exchange.
|
||||||
|
*/
|
||||||
|
h_wire_legacy?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash of the merchant's wire details.
|
* Hash of the merchant's wire details.
|
||||||
*/
|
*/
|
||||||
@ -662,7 +689,7 @@ export class ExchangeKeysJson {
|
|||||||
/**
|
/**
|
||||||
* List of offered denominations.
|
* List of offered denominations.
|
||||||
*/
|
*/
|
||||||
denoms: Denomination[];
|
denoms: ExchangeDenomination[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exchange's master public key.
|
* The exchange's master public key.
|
||||||
@ -672,7 +699,7 @@ export class ExchangeKeysJson {
|
|||||||
/**
|
/**
|
||||||
* The list of auditors (partially) auditing the exchange.
|
* The list of auditors (partially) auditing the exchange.
|
||||||
*/
|
*/
|
||||||
auditors: Auditor[];
|
auditors: ExchangeAuditor[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp when this response was issued.
|
* Timestamp when this response was issued.
|
||||||
@ -802,6 +829,7 @@ export class TipPickupGetResponse {
|
|||||||
export enum DenomKeyType {
|
export enum DenomKeyType {
|
||||||
Rsa = 1,
|
Rsa = 1,
|
||||||
ClauseSchnorr = 2,
|
ClauseSchnorr = 2,
|
||||||
|
LegacyRsa = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RsaBlindedDenominationSignature {
|
export interface RsaBlindedDenominationSignature {
|
||||||
@ -809,18 +837,25 @@ export interface RsaBlindedDenominationSignature {
|
|||||||
blinded_rsa_signature: string;
|
blinded_rsa_signature: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LegacyRsaBlindedDenominationSignature {
|
||||||
|
cipher: DenomKeyType.LegacyRsa;
|
||||||
|
blinded_rsa_signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CSBlindedDenominationSignature {
|
export interface CSBlindedDenominationSignature {
|
||||||
cipher: DenomKeyType.ClauseSchnorr;
|
cipher: DenomKeyType.ClauseSchnorr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BlindedDenominationSignature =
|
export type BlindedDenominationSignature =
|
||||||
| RsaBlindedDenominationSignature
|
| RsaBlindedDenominationSignature
|
||||||
| CSBlindedDenominationSignature;
|
| CSBlindedDenominationSignature
|
||||||
|
| LegacyRsaBlindedDenominationSignature;
|
||||||
|
|
||||||
export const codecForBlindedDenominationSignature = () =>
|
export const codecForBlindedDenominationSignature = () =>
|
||||||
buildCodecForUnion<BlindedDenominationSignature>()
|
buildCodecForUnion<BlindedDenominationSignature>()
|
||||||
.discriminateOn("cipher")
|
.discriminateOn("cipher")
|
||||||
.alternative(1, codecForRsaBlindedDenominationSignature())
|
.alternative(1, codecForRsaBlindedDenominationSignature())
|
||||||
|
.alternative(3, codecForLegacyRsaBlindedDenominationSignature())
|
||||||
.build("BlindedDenominationSignature");
|
.build("BlindedDenominationSignature");
|
||||||
|
|
||||||
export const codecForRsaBlindedDenominationSignature = () =>
|
export const codecForRsaBlindedDenominationSignature = () =>
|
||||||
@ -829,8 +864,17 @@ export const codecForRsaBlindedDenominationSignature = () =>
|
|||||||
.property("blinded_rsa_signature", codecForString())
|
.property("blinded_rsa_signature", codecForString())
|
||||||
.build("RsaBlindedDenominationSignature");
|
.build("RsaBlindedDenominationSignature");
|
||||||
|
|
||||||
|
export const codecForLegacyRsaBlindedDenominationSignature = () =>
|
||||||
|
buildCodecForObject<LegacyRsaBlindedDenominationSignature>()
|
||||||
|
.property("cipher", codecForConstNumber(1))
|
||||||
|
.property("blinded_rsa_signature", codecForString())
|
||||||
|
.build("LegacyRsaBlindedDenominationSignature");
|
||||||
|
|
||||||
export class WithdrawResponse {
|
export class WithdrawResponse {
|
||||||
ev_sig: BlindedDenominationSignature;
|
/**
|
||||||
|
* The string variant is for legacy protocol support.
|
||||||
|
*/
|
||||||
|
ev_sig: BlindedDenominationSignature | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -925,7 +969,10 @@ export interface ExchangeMeltResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeRevealItem {
|
export interface ExchangeRevealItem {
|
||||||
ev_sig: BlindedDenominationSignature;
|
/**
|
||||||
|
* The string variant is for the legacy v9 protocol.
|
||||||
|
*/
|
||||||
|
ev_sig: BlindedDenominationSignature | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeRevealResponse {
|
export interface ExchangeRevealResponse {
|
||||||
@ -1044,7 +1091,15 @@ export interface BankWithdrawalOperationPostResponse {
|
|||||||
transfer_done: boolean;
|
transfer_done: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
|
export type DenominationPubKey =
|
||||||
|
| RsaDenominationPubKey
|
||||||
|
| CsDenominationPubKey
|
||||||
|
| LegacyRsaDenominationPubKey;
|
||||||
|
|
||||||
|
export interface LegacyRsaDenominationPubKey {
|
||||||
|
cipher: DenomKeyType.LegacyRsa;
|
||||||
|
rsa_public_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RsaDenominationPubKey {
|
export interface RsaDenominationPubKey {
|
||||||
cipher: DenomKeyType.Rsa;
|
cipher: DenomKeyType.Rsa;
|
||||||
@ -1061,6 +1116,7 @@ export const codecForDenominationPubKey = () =>
|
|||||||
buildCodecForUnion<DenominationPubKey>()
|
buildCodecForUnion<DenominationPubKey>()
|
||||||
.discriminateOn("cipher")
|
.discriminateOn("cipher")
|
||||||
.alternative(1, codecForRsaDenominationPubKey())
|
.alternative(1, codecForRsaDenominationPubKey())
|
||||||
|
.alternative(3, codecForLegacyRsaDenominationPubKey())
|
||||||
.build("DenominationPubKey");
|
.build("DenominationPubKey");
|
||||||
|
|
||||||
export const codecForRsaDenominationPubKey = () =>
|
export const codecForRsaDenominationPubKey = () =>
|
||||||
@ -1069,6 +1125,12 @@ export const codecForRsaDenominationPubKey = () =>
|
|||||||
.property("rsa_public_key", codecForString())
|
.property("rsa_public_key", codecForString())
|
||||||
.build("DenominationPubKey");
|
.build("DenominationPubKey");
|
||||||
|
|
||||||
|
export const codecForLegacyRsaDenominationPubKey = () =>
|
||||||
|
buildCodecForObject<LegacyRsaDenominationPubKey>()
|
||||||
|
.property("cipher", codecForConstNumber(3))
|
||||||
|
.property("rsa_public_key", codecForString())
|
||||||
|
.build("LegacyRsaDenominationPubKey");
|
||||||
|
|
||||||
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> =>
|
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> =>
|
||||||
buildCodecForObject<BankWithdrawalOperationPostResponse>()
|
buildCodecForObject<BankWithdrawalOperationPostResponse>()
|
||||||
.property("transfer_done", codecForBoolean())
|
.property("transfer_done", codecForBoolean())
|
||||||
@ -1080,10 +1142,13 @@ export type EddsaSignatureString = string;
|
|||||||
export type EddsaPublicKeyString = string;
|
export type EddsaPublicKeyString = string;
|
||||||
export type CoinPublicKeyString = string;
|
export type CoinPublicKeyString = string;
|
||||||
|
|
||||||
export const codecForDenomination = (): Codec<Denomination> =>
|
export const codecForDenomination = (): Codec<ExchangeDenomination> =>
|
||||||
buildCodecForObject<Denomination>()
|
buildCodecForObject<ExchangeDenomination>()
|
||||||
.property("value", codecForString())
|
.property("value", codecForString())
|
||||||
.property("denom_pub", codecForDenominationPubKey())
|
.property(
|
||||||
|
"denom_pub",
|
||||||
|
codecForEither(codecForDenominationPubKey(), codecForString()),
|
||||||
|
)
|
||||||
.property("fee_withdraw", codecForString())
|
.property("fee_withdraw", codecForString())
|
||||||
.property("fee_deposit", codecForString())
|
.property("fee_deposit", codecForString())
|
||||||
.property("fee_refresh", codecForString())
|
.property("fee_refresh", codecForString())
|
||||||
@ -1101,8 +1166,8 @@ export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
|
|||||||
.property("auditor_sig", codecForString())
|
.property("auditor_sig", codecForString())
|
||||||
.build("AuditorDenomSig");
|
.build("AuditorDenomSig");
|
||||||
|
|
||||||
export const codecForAuditor = (): Codec<Auditor> =>
|
export const codecForAuditor = (): Codec<ExchangeAuditor> =>
|
||||||
buildCodecForObject<Auditor>()
|
buildCodecForObject<ExchangeAuditor>()
|
||||||
.property("auditor_pub", codecForString())
|
.property("auditor_pub", codecForString())
|
||||||
.property("auditor_url", codecForString())
|
.property("auditor_url", codecForString())
|
||||||
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
|
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
|
||||||
@ -1261,7 +1326,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
|
|||||||
.property("signkeys", codecForList(codecForExchangeSigningKey()))
|
.property("signkeys", codecForList(codecForExchangeSigningKey()))
|
||||||
.property("version", codecForString())
|
.property("version", codecForString())
|
||||||
.property("reserve_closing_delay", codecForDuration)
|
.property("reserve_closing_delay", codecForDuration)
|
||||||
.build("KeysJson");
|
.build("ExchangeKeysJson");
|
||||||
|
|
||||||
export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
|
export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
|
||||||
buildCodecForObject<WireFeesJson>()
|
buildCodecForObject<WireFeesJson>()
|
||||||
@ -1327,7 +1392,10 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
|
|||||||
|
|
||||||
export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
|
export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
|
||||||
buildCodecForObject<WithdrawResponse>()
|
buildCodecForObject<WithdrawResponse>()
|
||||||
.property("ev_sig", codecForBlindedDenominationSignature())
|
.property(
|
||||||
|
"ev_sig",
|
||||||
|
codecForEither(codecForBlindedDenominationSignature(), codecForString()),
|
||||||
|
)
|
||||||
.build("WithdrawResponse");
|
.build("WithdrawResponse");
|
||||||
|
|
||||||
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
|
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
|
||||||
@ -1345,7 +1413,10 @@ export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
|
|||||||
|
|
||||||
export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
|
export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
|
||||||
buildCodecForObject<ExchangeRevealItem>()
|
buildCodecForObject<ExchangeRevealItem>()
|
||||||
.property("ev_sig", codecForBlindedDenominationSignature())
|
.property(
|
||||||
|
"ev_sig",
|
||||||
|
codecForEither(codecForBlindedDenominationSignature(), codecForString()),
|
||||||
|
)
|
||||||
.build("ExchangeRevealItem");
|
.build("ExchangeRevealItem");
|
||||||
|
|
||||||
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> =>
|
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> =>
|
||||||
|
@ -49,6 +49,7 @@ import {
|
|||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
|
DenomKeyType,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
} from "./talerTypes.js";
|
} from "./talerTypes.js";
|
||||||
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
||||||
@ -515,6 +516,7 @@ export interface DepositInfo {
|
|||||||
merchantPub: string;
|
merchantPub: string;
|
||||||
feeDeposit: AmountJson;
|
feeDeposit: AmountJson;
|
||||||
wireInfoHash: string;
|
wireInfoHash: string;
|
||||||
|
denomKeyType: DenomKeyType;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
}
|
}
|
||||||
|
@ -1173,6 +1173,17 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runAggregatorOnce() {
|
async runAggregatorOnce() {
|
||||||
|
try {
|
||||||
|
await runCommand(
|
||||||
|
this.globalState,
|
||||||
|
`exchange-${this.name}-aggregator-once`,
|
||||||
|
"taler-exchange-aggregator",
|
||||||
|
[...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
"running aggregator with KYC off didn't work, might be old version, running again",
|
||||||
|
);
|
||||||
await runCommand(
|
await runCommand(
|
||||||
this.globalState,
|
this.globalState,
|
||||||
`exchange-${this.name}-aggregator-once`,
|
`exchange-${this.name}-aggregator-once`,
|
||||||
@ -1180,6 +1191,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
[...this.timetravelArgArr, "-c", this.configFilename, "-t"],
|
[...this.timetravelArgArr, "-c", this.configFilename, "-t"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async runTransferOnce() {
|
async runTransferOnce() {
|
||||||
await runCommand(
|
await runCommand(
|
||||||
|
@ -1018,6 +1018,13 @@ const testCli = walletCli.subcommand("testingArgs", "testing", {
|
|||||||
help: "Subcommands for testing.",
|
help: "Subcommands for testing.",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testCli.subcommand("logtest", "logtest").action(async (args) => {
|
||||||
|
logger.trace("This is a trace message.");
|
||||||
|
logger.info("This is an info message.");
|
||||||
|
logger.warn("This is an warning message.");
|
||||||
|
logger.error("This is an error message.");
|
||||||
|
});
|
||||||
|
|
||||||
testCli
|
testCli
|
||||||
.subcommand("listIntegrationtests", "list-integrationtests")
|
.subcommand("listIntegrationtests", "list-integrationtests")
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
|
@ -52,8 +52,7 @@ export interface TrustInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantInfo {
|
export interface MerchantInfo {
|
||||||
supportsMerchantProtocolV1: boolean;
|
protocolVersionCurrent: number;
|
||||||
supportsMerchantProtocolV2: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -392,6 +392,7 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isValidWireAccount(
|
isValidWireAccount(
|
||||||
|
versionCurrent: number,
|
||||||
paytoUri: string,
|
paytoUri: string,
|
||||||
sig: string,
|
sig: string,
|
||||||
masterPub: string,
|
masterPub: string,
|
||||||
@ -399,6 +400,7 @@ export class CryptoApi {
|
|||||||
return this.doRpc<boolean>(
|
return this.doRpc<boolean>(
|
||||||
"isValidWireAccount",
|
"isValidWireAccount",
|
||||||
4,
|
4,
|
||||||
|
versionCurrent,
|
||||||
paytoUri,
|
paytoUri,
|
||||||
sig,
|
sig,
|
||||||
masterPub,
|
masterPub,
|
||||||
|
@ -154,9 +154,10 @@ export class CryptoImplementation {
|
|||||||
* reserve.
|
* reserve.
|
||||||
*/
|
*/
|
||||||
createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
|
createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
|
||||||
if (req.denomPub.cipher !== 1) {
|
if (
|
||||||
throw Error("unsupported cipher");
|
req.denomPub.cipher === DenomKeyType.Rsa ||
|
||||||
}
|
req.denomPub.cipher === DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
const reservePub = decodeCrock(req.reservePub);
|
const reservePub = decodeCrock(req.reservePub);
|
||||||
const reservePriv = decodeCrock(req.reservePriv);
|
const reservePriv = decodeCrock(req.reservePriv);
|
||||||
const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
|
const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
|
||||||
@ -188,7 +189,7 @@ export class CryptoImplementation {
|
|||||||
coinPub: encodeCrock(derivedPlanchet.coinPub),
|
coinPub: encodeCrock(derivedPlanchet.coinPub),
|
||||||
coinValue: req.value,
|
coinValue: req.value,
|
||||||
denomPub: {
|
denomPub: {
|
||||||
cipher: 1,
|
cipher: req.denomPub.cipher,
|
||||||
rsa_public_key: encodeCrock(denomPubRsa),
|
rsa_public_key: encodeCrock(denomPubRsa),
|
||||||
},
|
},
|
||||||
denomPubHash: encodeCrock(denomPubHash),
|
denomPubHash: encodeCrock(denomPubHash),
|
||||||
@ -197,13 +198,19 @@ export class CryptoImplementation {
|
|||||||
coinEvHash: encodeCrock(evHash),
|
coinEvHash: encodeCrock(evHash),
|
||||||
};
|
};
|
||||||
return planchet;
|
return planchet;
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported cipher, unable to create planchet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a planchet used for tipping, including the private keys.
|
* Create a planchet used for tipping, including the private keys.
|
||||||
*/
|
*/
|
||||||
createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
|
createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
|
||||||
if (req.denomPub.cipher !== 1) {
|
if (
|
||||||
|
req.denomPub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
req.denomPub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
|
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
|
||||||
@ -243,6 +250,19 @@ export class CryptoImplementation {
|
|||||||
|
|
||||||
const coinPriv = decodeCrock(coin.coinPriv);
|
const coinPriv = decodeCrock(coin.coinPriv);
|
||||||
const coinSig = eddsaSign(p, coinPriv);
|
const coinSig = eddsaSign(p, coinPriv);
|
||||||
|
if (coin.denomPub.cipher === DenomKeyType.LegacyRsa) {
|
||||||
|
logger.info("creating legacy recoup request");
|
||||||
|
const paybackRequest: RecoupRequest = {
|
||||||
|
coin_blind_key_secret: coin.blindingKey,
|
||||||
|
coin_pub: coin.coinPub,
|
||||||
|
coin_sig: encodeCrock(coinSig),
|
||||||
|
denom_pub_hash: coin.denomPubHash,
|
||||||
|
denom_sig: coin.denomSig.rsa_signature,
|
||||||
|
refreshed: coin.coinSource.type === CoinSourceType.Refresh,
|
||||||
|
};
|
||||||
|
return paybackRequest;
|
||||||
|
} else {
|
||||||
|
logger.info("creating v10 recoup request");
|
||||||
const paybackRequest: RecoupRequest = {
|
const paybackRequest: RecoupRequest = {
|
||||||
coin_blind_key_secret: coin.blindingKey,
|
coin_blind_key_secret: coin.blindingKey,
|
||||||
coin_pub: coin.coinPub,
|
coin_pub: coin.coinPub,
|
||||||
@ -253,6 +273,7 @@ export class CryptoImplementation {
|
|||||||
};
|
};
|
||||||
return paybackRequest;
|
return paybackRequest;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a payment signature is valid.
|
* Check if a payment signature is valid.
|
||||||
@ -326,15 +347,31 @@ export class CryptoImplementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isValidWireAccount(
|
isValidWireAccount(
|
||||||
|
versionCurrent: number,
|
||||||
paytoUri: string,
|
paytoUri: string,
|
||||||
sig: string,
|
sig: string,
|
||||||
masterPub: string,
|
masterPub: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (versionCurrent === 10) {
|
||||||
const paytoHash = hash(stringToBytes(paytoUri + "\0"));
|
const paytoHash = hash(stringToBytes(paytoUri + "\0"));
|
||||||
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
||||||
.put(paytoHash)
|
.put(paytoHash)
|
||||||
.build();
|
.build();
|
||||||
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
|
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
|
||||||
|
} else if (versionCurrent === 9) {
|
||||||
|
const h = kdf(
|
||||||
|
64,
|
||||||
|
stringToBytes("exchange-wire-signature"),
|
||||||
|
stringToBytes(paytoUri + "\0"),
|
||||||
|
new Uint8Array(0),
|
||||||
|
);
|
||||||
|
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
||||||
|
.put(h)
|
||||||
|
.build();
|
||||||
|
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
|
||||||
|
} else {
|
||||||
|
throw Error(`unsupported version (${versionCurrent})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidContractTermsSignature(
|
isValidContractTermsSignature(
|
||||||
@ -393,7 +430,10 @@ export class CryptoImplementation {
|
|||||||
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
|
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
|
||||||
// FIXME: put extensions here if used
|
// FIXME: put extensions here if used
|
||||||
const hExt = new Uint8Array(64);
|
const hExt = new Uint8Array(64);
|
||||||
const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
|
let d: Uint8Array;
|
||||||
|
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
|
||||||
|
logger.warn("signing v10 deposit permission");
|
||||||
|
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
|
||||||
.put(decodeCrock(depositInfo.contractTermsHash))
|
.put(decodeCrock(depositInfo.contractTermsHash))
|
||||||
.put(hExt)
|
.put(hExt)
|
||||||
.put(decodeCrock(depositInfo.wireInfoHash))
|
.put(decodeCrock(depositInfo.wireInfoHash))
|
||||||
@ -404,8 +444,25 @@ export class CryptoImplementation {
|
|||||||
.put(amountToBuffer(depositInfo.feeDeposit))
|
.put(amountToBuffer(depositInfo.feeDeposit))
|
||||||
.put(decodeCrock(depositInfo.merchantPub))
|
.put(decodeCrock(depositInfo.merchantPub))
|
||||||
.build();
|
.build();
|
||||||
|
} else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) {
|
||||||
|
logger.warn("signing legacy deposit permission");
|
||||||
|
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
|
||||||
|
.put(decodeCrock(depositInfo.contractTermsHash))
|
||||||
|
.put(decodeCrock(depositInfo.wireInfoHash))
|
||||||
|
.put(decodeCrock(depositInfo.denomPubHash))
|
||||||
|
.put(timestampRoundedToBuffer(depositInfo.timestamp))
|
||||||
|
.put(timestampRoundedToBuffer(depositInfo.refundDeadline))
|
||||||
|
.put(amountToBuffer(depositInfo.spendAmount))
|
||||||
|
.put(amountToBuffer(depositInfo.feeDeposit))
|
||||||
|
.put(decodeCrock(depositInfo.merchantPub))
|
||||||
|
.put(decodeCrock(depositInfo.coinPub))
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported exchange protocol version");
|
||||||
|
}
|
||||||
const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
|
const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
|
||||||
|
|
||||||
|
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
|
||||||
const s: CoinDepositPermission = {
|
const s: CoinDepositPermission = {
|
||||||
coin_pub: depositInfo.coinPub,
|
coin_pub: depositInfo.coinPub,
|
||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
@ -418,6 +475,19 @@ export class CryptoImplementation {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
return s;
|
return s;
|
||||||
|
} else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) {
|
||||||
|
const s: CoinDepositPermission = {
|
||||||
|
coin_pub: depositInfo.coinPub,
|
||||||
|
coin_sig: encodeCrock(coinSig),
|
||||||
|
contribution: Amounts.stringify(depositInfo.spendAmount),
|
||||||
|
h_denom: depositInfo.denomPubHash,
|
||||||
|
exchange_url: depositInfo.exchangeBaseUrl,
|
||||||
|
ub_sig: depositInfo.denomSig.rsa_signature,
|
||||||
|
};
|
||||||
|
return s;
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported merchant protocol version");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deriveRefreshSession(
|
async deriveRefreshSession(
|
||||||
@ -466,12 +536,14 @@ export class CryptoImplementation {
|
|||||||
|
|
||||||
for (const denomSel of newCoinDenoms) {
|
for (const denomSel of newCoinDenoms) {
|
||||||
for (let i = 0; i < denomSel.count; i++) {
|
for (let i = 0; i < denomSel.count; i++) {
|
||||||
if (denomSel.denomPub.cipher !== 1) {
|
if (denomSel.denomPub.cipher === DenomKeyType.LegacyRsa) {
|
||||||
throw Error("unsupported cipher");
|
const r = decodeCrock(denomSel.denomPub.rsa_public_key);
|
||||||
}
|
sessionHc.update(r);
|
||||||
|
} else {
|
||||||
sessionHc.update(hashDenomPub(denomSel.denomPub));
|
sessionHc.update(hashDenomPub(denomSel.denomPub));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sessionHc.update(decodeCrock(meltCoinPub));
|
sessionHc.update(decodeCrock(meltCoinPub));
|
||||||
sessionHc.update(amountToBuffer(valueWithFee));
|
sessionHc.update(amountToBuffer(valueWithFee));
|
||||||
@ -508,8 +580,11 @@ export class CryptoImplementation {
|
|||||||
blindingFactor = fresh.bks;
|
blindingFactor = fresh.bks;
|
||||||
}
|
}
|
||||||
const pubHash = hash(coinPub);
|
const pubHash = hash(coinPub);
|
||||||
if (denomSel.denomPub.cipher !== 1) {
|
if (
|
||||||
throw Error("unsupported cipher");
|
denomSel.denomPub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
denomSel.denomPub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
|
throw Error("unsupported cipher, can't create refresh session");
|
||||||
}
|
}
|
||||||
const denomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
|
const denomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
|
||||||
const ev = rsaBlind(pubHash, blindingFactor, denomPub);
|
const ev = rsaBlind(pubHash, blindingFactor, denomPub);
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
AmountJson,
|
AmountJson,
|
||||||
AmountString,
|
AmountString,
|
||||||
Auditor,
|
ExchangeAuditor,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
@ -427,7 +427,7 @@ export interface ExchangeDetailsRecord {
|
|||||||
/**
|
/**
|
||||||
* Auditors (partially) auditing the exchange.
|
* Auditors (partially) auditing the exchange.
|
||||||
*/
|
*/
|
||||||
auditors: Auditor[];
|
auditors: ExchangeAuditor[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last observed protocol version.
|
* Last observed protocol version.
|
||||||
@ -1136,6 +1136,7 @@ export interface WalletContractData {
|
|||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
wireMethod: string;
|
wireMethod: string;
|
||||||
wireInfoHash: string;
|
wireInfoHash: string;
|
||||||
|
wireInfoLegacyHash?: string;
|
||||||
maxDepositFee: AmountJson;
|
maxDepositFee: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
BackupRefundState,
|
BackupRefundState,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
BackupRefreshReason,
|
BackupRefreshReason,
|
||||||
|
DenomKeyType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
@ -331,7 +332,10 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const backupDenomination of backupExchangeDetails.denominations) {
|
for (const backupDenomination of backupExchangeDetails.denominations) {
|
||||||
if (backupDenomination.denom_pub.cipher !== 1) {
|
if (
|
||||||
|
backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
backupDenomination.denom_pub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
const denomPubHash =
|
const denomPubHash =
|
||||||
|
@ -38,14 +38,15 @@ import {
|
|||||||
codecForString,
|
codecForString,
|
||||||
codecOptional,
|
codecOptional,
|
||||||
ConfirmPayResultType,
|
ConfirmPayResultType,
|
||||||
|
DenomKeyType,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
hashDenomPub,
|
hashDenomPub,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
j2s,
|
j2s,
|
||||||
|
LibtoolVersion,
|
||||||
Logger,
|
Logger,
|
||||||
notEmpty,
|
notEmpty,
|
||||||
NotificationType,
|
|
||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
RecoveryLoadRequest,
|
RecoveryLoadRequest,
|
||||||
RecoveryMergeStrategy,
|
RecoveryMergeStrategy,
|
||||||
@ -167,7 +168,10 @@ async function computeBackupCryptoData(
|
|||||||
};
|
};
|
||||||
for (const backupExchangeDetails of backupContent.exchange_details) {
|
for (const backupExchangeDetails of backupContent.exchange_details) {
|
||||||
for (const backupDenom of backupExchangeDetails.denominations) {
|
for (const backupDenom of backupExchangeDetails.denominations) {
|
||||||
if (backupDenom.denom_pub.cipher !== 1) {
|
if (
|
||||||
|
backupDenom.denom_pub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
backupDenom.denom_pub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
for (const backupCoin of backupDenom.coins) {
|
for (const backupCoin of backupDenom.coins) {
|
||||||
@ -184,9 +188,25 @@ async function computeBackupCryptoData(
|
|||||||
coinPub,
|
coinPub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
LibtoolVersion.compare(backupExchangeDetails.protocol_version, "9")
|
||||||
|
?.compatible
|
||||||
|
) {
|
||||||
|
cryptoData.rsaDenomPubToHash[
|
||||||
|
backupDenom.denom_pub.rsa_public_key
|
||||||
|
] = encodeCrock(
|
||||||
|
hash(decodeCrock(backupDenom.denom_pub.rsa_public_key)),
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
LibtoolVersion.compare(backupExchangeDetails.protocol_version, "10")
|
||||||
|
?.compatible
|
||||||
|
) {
|
||||||
cryptoData.rsaDenomPubToHash[
|
cryptoData.rsaDenomPubToHash[
|
||||||
backupDenom.denom_pub.rsa_public_key
|
backupDenom.denom_pub.rsa_public_key
|
||||||
] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
|
] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported exchange protocol version");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const backupReserve of backupExchangeDetails.reserves) {
|
for (const backupReserve of backupExchangeDetails.reserves) {
|
||||||
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
CreateDepositGroupRequest,
|
CreateDepositGroupRequest,
|
||||||
CreateDepositGroupResponse,
|
CreateDepositGroupResponse,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
|
DenomKeyType,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
Logger,
|
Logger,
|
||||||
@ -59,6 +60,8 @@ import {
|
|||||||
getCandidatePayCoins,
|
getCandidatePayCoins,
|
||||||
getEffectiveDepositAmount,
|
getEffectiveDepositAmount,
|
||||||
getTotalPaymentCost,
|
getTotalPaymentCost,
|
||||||
|
hashWire,
|
||||||
|
hashWireLegacy,
|
||||||
} from "./pay.js";
|
} from "./pay.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,16 +106,6 @@ const codecForDepositSuccess = (): Codec<DepositSuccess> =>
|
|||||||
.property("transaction_base_url", codecOptional(codecForString()))
|
.property("transaction_base_url", codecOptional(codecForString()))
|
||||||
.build("DepositSuccess");
|
.build("DepositSuccess");
|
||||||
|
|
||||||
function hashWire(paytoUri: string, salt: string): string {
|
|
||||||
const r = kdf(
|
|
||||||
64,
|
|
||||||
stringToBytes(paytoUri + "\0"),
|
|
||||||
decodeCrock(salt),
|
|
||||||
stringToBytes("merchant-wire-signature"),
|
|
||||||
);
|
|
||||||
return encodeCrock(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetDepositGroupRetry(
|
async function resetDepositGroupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
depositGroupId: string,
|
depositGroupId: string,
|
||||||
@ -211,8 +204,34 @@ async function processDepositGroupImpl(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const perm = depositPermissions[i];
|
const perm = depositPermissions[i];
|
||||||
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
|
let requestBody: any;
|
||||||
const httpResp = await ws.http.postJson(url.href, {
|
if (
|
||||||
|
typeof perm.ub_sig === "string" ||
|
||||||
|
perm.ub_sig.cipher === DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
|
// Legacy request
|
||||||
|
logger.info("creating legacy deposit request");
|
||||||
|
const wireHash = hashWireLegacy(
|
||||||
|
depositGroup.wire.payto_uri,
|
||||||
|
depositGroup.wire.salt,
|
||||||
|
);
|
||||||
|
requestBody = {
|
||||||
|
contribution: Amounts.stringify(perm.contribution),
|
||||||
|
wire: depositGroup.wire,
|
||||||
|
h_wire: wireHash,
|
||||||
|
h_contract_terms: depositGroup.contractTermsHash,
|
||||||
|
ub_sig: perm.ub_sig,
|
||||||
|
timestamp: depositGroup.contractTermsRaw.timestamp,
|
||||||
|
wire_transfer_deadline:
|
||||||
|
depositGroup.contractTermsRaw.wire_transfer_deadline,
|
||||||
|
refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
|
||||||
|
coin_sig: perm.coin_sig,
|
||||||
|
denom_pub_hash: perm.h_denom,
|
||||||
|
merchant_pub: depositGroup.merchantPub,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
logger.info("creating v10 deposit request");
|
||||||
|
requestBody = {
|
||||||
contribution: Amounts.stringify(perm.contribution),
|
contribution: Amounts.stringify(perm.contribution),
|
||||||
merchant_payto_uri: depositGroup.wire.payto_uri,
|
merchant_payto_uri: depositGroup.wire.payto_uri,
|
||||||
wire_salt: depositGroup.wire.salt,
|
wire_salt: depositGroup.wire.salt,
|
||||||
@ -225,7 +244,10 @@ async function processDepositGroupImpl(
|
|||||||
coin_sig: perm.coin_sig,
|
coin_sig: perm.coin_sig,
|
||||||
denom_pub_hash: perm.h_denom,
|
denom_pub_hash: perm.h_denom,
|
||||||
merchant_pub: depositGroup.merchantPub,
|
merchant_pub: depositGroup.merchantPub,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
|
||||||
|
const httpResp = await ws.http.postJson(url.href, requestBody);
|
||||||
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
||||||
@ -358,6 +380,7 @@ export async function createDepositGroup(
|
|||||||
const merchantPair = await ws.cryptoApi.createEddsaKeypair();
|
const merchantPair = await ws.cryptoApi.createEddsaKeypair();
|
||||||
const wireSalt = encodeCrock(getRandomBytes(16));
|
const wireSalt = encodeCrock(getRandomBytes(16));
|
||||||
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
|
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
|
||||||
|
const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt);
|
||||||
const contractTerms: ContractTerms = {
|
const contractTerms: ContractTerms = {
|
||||||
auditors: [],
|
auditors: [],
|
||||||
exchanges: exchangeInfos,
|
exchanges: exchangeInfos,
|
||||||
@ -371,7 +394,10 @@ export async function createDepositGroup(
|
|||||||
nonce: noncePair.pub,
|
nonce: noncePair.pub,
|
||||||
wire_transfer_deadline: timestampRound,
|
wire_transfer_deadline: timestampRound,
|
||||||
order_id: "",
|
order_id: "",
|
||||||
|
// This is always the v2 wire hash, as we're the "merchant" and support v2.
|
||||||
h_wire: wireHash,
|
h_wire: wireHash,
|
||||||
|
// Required for older exchanges.
|
||||||
|
h_wire_legacy: wireHashLegacy,
|
||||||
pay_deadline: timestampAddDuration(
|
pay_deadline: timestampAddDuration(
|
||||||
timestampRound,
|
timestampRound,
|
||||||
durationFromSpec({ hours: 1 }),
|
durationFromSpec({ hours: 1 }),
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
Auditor,
|
ExchangeAuditor,
|
||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
codecForExchangeKeysJson,
|
codecForExchangeKeysJson,
|
||||||
codecForExchangeWireJson,
|
codecForExchangeWireJson,
|
||||||
Denomination,
|
ExchangeDenomination,
|
||||||
Duration,
|
Duration,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
ExchangeSignKeyJson,
|
ExchangeSignKeyJson,
|
||||||
@ -40,6 +40,9 @@ import {
|
|||||||
Timestamp,
|
Timestamp,
|
||||||
hashDenomPub,
|
hashDenomPub,
|
||||||
LibtoolVersion,
|
LibtoolVersion,
|
||||||
|
codecForAny,
|
||||||
|
DenominationPubKey,
|
||||||
|
DenomKeyType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
|
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
|
||||||
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
|
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
|
||||||
@ -77,11 +80,21 @@ function denominationRecordFromKeys(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
exchangeMasterPub: string,
|
exchangeMasterPub: string,
|
||||||
listIssueDate: Timestamp,
|
listIssueDate: Timestamp,
|
||||||
denomIn: Denomination,
|
denomIn: ExchangeDenomination,
|
||||||
): DenominationRecord {
|
): DenominationRecord {
|
||||||
const denomPubHash = encodeCrock(hashDenomPub(denomIn.denom_pub));
|
let denomPub: DenominationPubKey;
|
||||||
|
// We support exchange protocol v9 and v10.
|
||||||
|
if (typeof denomIn.denom_pub === "string") {
|
||||||
|
denomPub = {
|
||||||
|
cipher: DenomKeyType.LegacyRsa,
|
||||||
|
rsa_public_key: denomIn.denom_pub,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
denomPub = denomIn.denom_pub;
|
||||||
|
}
|
||||||
|
const denomPubHash = encodeCrock(hashDenomPub(denomPub));
|
||||||
const d: DenominationRecord = {
|
const d: DenominationRecord = {
|
||||||
denomPub: denomIn.denom_pub,
|
denomPub,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
exchangeMasterPub,
|
exchangeMasterPub,
|
||||||
@ -205,6 +218,7 @@ export async function acceptExchangeTermsOfService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function validateWireInfo(
|
async function validateWireInfo(
|
||||||
|
versionCurrent: number,
|
||||||
wireInfo: ExchangeWireJson,
|
wireInfo: ExchangeWireJson,
|
||||||
masterPublicKey: string,
|
masterPublicKey: string,
|
||||||
cryptoApi: CryptoApi,
|
cryptoApi: CryptoApi,
|
||||||
@ -212,6 +226,7 @@ async function validateWireInfo(
|
|||||||
for (const a of wireInfo.accounts) {
|
for (const a of wireInfo.accounts) {
|
||||||
logger.trace("validating exchange acct");
|
logger.trace("validating exchange acct");
|
||||||
const isValid = await cryptoApi.isValidWireAccount(
|
const isValid = await cryptoApi.isValidWireAccount(
|
||||||
|
versionCurrent,
|
||||||
a.payto_uri,
|
a.payto_uri,
|
||||||
a.master_sig,
|
a.master_sig,
|
||||||
masterPublicKey,
|
masterPublicKey,
|
||||||
@ -321,7 +336,7 @@ async function provideExchangeRecord(
|
|||||||
interface ExchangeKeysDownloadResult {
|
interface ExchangeKeysDownloadResult {
|
||||||
masterPublicKey: string;
|
masterPublicKey: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
auditors: Auditor[];
|
auditors: ExchangeAuditor[];
|
||||||
currentDenominations: DenominationRecord[];
|
currentDenominations: DenominationRecord[];
|
||||||
protocolVersion: string;
|
protocolVersion: string;
|
||||||
signingKeys: ExchangeSignKeyJson[];
|
signingKeys: ExchangeSignKeyJson[];
|
||||||
@ -345,14 +360,14 @@ async function downloadKeysInfo(
|
|||||||
const resp = await http.get(keysUrl.href, {
|
const resp = await http.get(keysUrl.href, {
|
||||||
timeout,
|
timeout,
|
||||||
});
|
});
|
||||||
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForExchangeKeysJson(),
|
codecForExchangeKeysJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("received /keys response");
|
logger.info("received /keys response");
|
||||||
|
|
||||||
if (exchangeKeysJson.denoms.length === 0) {
|
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
|
||||||
const opErr = makeErrorDetails(
|
const opErr = makeErrorDetails(
|
||||||
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
||||||
"exchange doesn't offer any denominations",
|
"exchange doesn't offer any denominations",
|
||||||
@ -363,7 +378,7 @@ async function downloadKeysInfo(
|
|||||||
throw new OperationFailedError(opErr);
|
throw new OperationFailedError(opErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolVersion = exchangeKeysJson.version;
|
const protocolVersion = exchangeKeysJsonUnchecked.version;
|
||||||
|
|
||||||
const versionRes = LibtoolVersion.compare(
|
const versionRes = LibtoolVersion.compare(
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
@ -382,29 +397,29 @@ async function downloadKeysInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currency = Amounts.parseOrThrow(
|
const currency = Amounts.parseOrThrow(
|
||||||
exchangeKeysJson.denoms[0].value,
|
exchangeKeysJsonUnchecked.denoms[0].value,
|
||||||
).currency.toUpperCase();
|
).currency.toUpperCase();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
|
||||||
currency,
|
currency,
|
||||||
auditors: exchangeKeysJson.auditors,
|
auditors: exchangeKeysJsonUnchecked.auditors,
|
||||||
currentDenominations: exchangeKeysJson.denoms.map((d) =>
|
currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
|
||||||
denominationRecordFromKeys(
|
denominationRecordFromKeys(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
exchangeKeysJson.master_public_key,
|
exchangeKeysJsonUnchecked.master_public_key,
|
||||||
exchangeKeysJson.list_issue_date,
|
exchangeKeysJsonUnchecked.list_issue_date,
|
||||||
d,
|
d,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
protocolVersion: exchangeKeysJson.version,
|
protocolVersion: exchangeKeysJsonUnchecked.version,
|
||||||
signingKeys: exchangeKeysJson.signkeys,
|
signingKeys: exchangeKeysJsonUnchecked.signkeys,
|
||||||
reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
|
reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
|
||||||
expiry: getExpiryTimestamp(resp, {
|
expiry: getExpiryTimestamp(resp, {
|
||||||
minDuration: durationFromSpec({ hours: 1 }),
|
minDuration: durationFromSpec({ hours: 1 }),
|
||||||
}),
|
}),
|
||||||
recoup: exchangeKeysJson.recoup ?? [],
|
recoup: exchangeKeysJsonUnchecked.recoup ?? [],
|
||||||
listIssueDate: exchangeKeysJson.list_issue_date,
|
listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,7 +481,14 @@ async function updateExchangeFromUrlImpl(
|
|||||||
|
|
||||||
logger.info("validating exchange /wire info");
|
logger.info("validating exchange /wire info");
|
||||||
|
|
||||||
|
const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
|
||||||
|
if (!version) {
|
||||||
|
// Should have been validated earlier.
|
||||||
|
throw Error("unexpected invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
const wireInfo = await validateWireInfo(
|
const wireInfo = await validateWireInfo(
|
||||||
|
version.current,
|
||||||
wireInfoDownload,
|
wireInfoDownload,
|
||||||
keysInfo.masterPublicKey,
|
keysInfo.masterPublicKey,
|
||||||
ws.cryptoApi,
|
ws.cryptoApi,
|
||||||
|
@ -52,15 +52,13 @@ export async function getMerchantInfo(
|
|||||||
`merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
|
`merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const parsedVersion = LibtoolVersion.parseVersion(configResp.version);
|
||||||
|
if (!parsedVersion) {
|
||||||
|
throw Error("invalid merchant version");
|
||||||
|
}
|
||||||
|
|
||||||
const merchantInfo: MerchantInfo = {
|
const merchantInfo: MerchantInfo = {
|
||||||
supportsMerchantProtocolV1: !!LibtoolVersion.compare(
|
protocolVersionCurrent: parsedVersion.current,
|
||||||
"1:0:0",
|
|
||||||
configResp.version,
|
|
||||||
)?.compatible,
|
|
||||||
supportsMerchantProtocolV2: !!LibtoolVersion.compare(
|
|
||||||
"2:0:0",
|
|
||||||
configResp.version,
|
|
||||||
)?.compatible,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.merchantInfoCache[canonBaseUrl] = merchantInfo;
|
ws.merchantInfoCache[canonBaseUrl] = merchantInfo;
|
||||||
|
@ -54,6 +54,10 @@ import {
|
|||||||
URL,
|
URL,
|
||||||
getDurationRemaining,
|
getDurationRemaining,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
|
DenomKeyType,
|
||||||
|
kdf,
|
||||||
|
stringToBytes,
|
||||||
|
decodeCrock,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
|
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -108,6 +112,26 @@ import {
|
|||||||
*/
|
*/
|
||||||
const logger = new Logger("pay.ts");
|
const logger = new Logger("pay.ts");
|
||||||
|
|
||||||
|
export function hashWire(paytoUri: string, salt: string): string {
|
||||||
|
const r = kdf(
|
||||||
|
64,
|
||||||
|
stringToBytes(paytoUri + "\0"),
|
||||||
|
decodeCrock(salt),
|
||||||
|
stringToBytes("merchant-wire-signature"),
|
||||||
|
);
|
||||||
|
return encodeCrock(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashWireLegacy(paytoUri: string, salt: string): string {
|
||||||
|
const r = kdf(
|
||||||
|
64,
|
||||||
|
stringToBytes(paytoUri + "\0"),
|
||||||
|
stringToBytes(salt + "\0"),
|
||||||
|
stringToBytes("merchant-wire-signature"),
|
||||||
|
);
|
||||||
|
return encodeCrock(r);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the total cost of a payment to the customer.
|
* Compute the total cost of a payment to the customer.
|
||||||
*
|
*
|
||||||
@ -669,6 +693,7 @@ export function extractContractData(
|
|||||||
timestamp: parsedContractTerms.timestamp,
|
timestamp: parsedContractTerms.timestamp,
|
||||||
wireMethod: parsedContractTerms.wire_method,
|
wireMethod: parsedContractTerms.wire_method,
|
||||||
wireInfoHash: parsedContractTerms.h_wire,
|
wireInfoHash: parsedContractTerms.h_wire,
|
||||||
|
wireInfoLegacyHash: parsedContractTerms.h_wire_legacy,
|
||||||
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
||||||
merchant: parsedContractTerms.merchant,
|
merchant: parsedContractTerms.merchant,
|
||||||
products: parsedContractTerms.products,
|
products: parsedContractTerms.products,
|
||||||
@ -882,7 +907,6 @@ async function startDownloadProposal(
|
|||||||
claimToken: string | undefined,
|
claimToken: string | undefined,
|
||||||
noncePriv: string | undefined,
|
noncePriv: string | undefined,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
|
||||||
const oldProposal = await ws.db
|
const oldProposal = await ws.db
|
||||||
.mktx((x) => ({ proposals: x.proposals }))
|
.mktx((x) => ({ proposals: x.proposals }))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -896,15 +920,19 @@ async function startDownloadProposal(
|
|||||||
* If we have already claimed this proposal with the same sessionId
|
* If we have already claimed this proposal with the same sessionId
|
||||||
* nonce and claim token, reuse it.
|
* nonce and claim token, reuse it.
|
||||||
*/
|
*/
|
||||||
if (oldProposal &&
|
if (
|
||||||
|
oldProposal &&
|
||||||
oldProposal.downloadSessionId === sessionId &&
|
oldProposal.downloadSessionId === sessionId &&
|
||||||
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
||||||
oldProposal.claimToken === claimToken) {
|
oldProposal.claimToken === claimToken
|
||||||
|
) {
|
||||||
await processDownloadProposal(ws, oldProposal.proposalId);
|
await processDownloadProposal(ws, oldProposal.proposalId);
|
||||||
return oldProposal.proposalId;
|
return oldProposal.proposalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { priv, pub } = await (noncePriv ? ws.cryptoApi.eddsaGetPublic(noncePriv) : ws.cryptoApi.createEddsaKeypair());
|
const { priv, pub } = await (noncePriv
|
||||||
|
? ws.cryptoApi.eddsaGetPublic(noncePriv)
|
||||||
|
: ws.cryptoApi.createEddsaKeypair());
|
||||||
const proposalId = encodeCrock(getRandomBytes(32));
|
const proposalId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
const proposalRecord: ProposalRecord = {
|
const proposalRecord: ProposalRecord = {
|
||||||
@ -1169,6 +1197,11 @@ async function submitPay(
|
|||||||
|
|
||||||
logger.trace("paying with session ID", sessionId);
|
logger.trace("paying with session ID", sessionId);
|
||||||
|
|
||||||
|
const merchantInfo = await ws.merchantOps.getMerchantInfo(
|
||||||
|
ws,
|
||||||
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
if (!purchase.merchantPaySig) {
|
if (!purchase.merchantPaySig) {
|
||||||
const payUrl = new URL(
|
const payUrl = new URL(
|
||||||
`orders/${purchase.download.contractData.orderId}/pay`,
|
`orders/${purchase.download.contractData.orderId}/pay`,
|
||||||
@ -1568,11 +1601,21 @@ export async function generateDepositPermissions(
|
|||||||
|
|
||||||
for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
|
for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
|
||||||
const { coin, denom } = coinWithDenom[i];
|
const { coin, denom } = coinWithDenom[i];
|
||||||
|
let wireInfoHash: string;
|
||||||
|
if (
|
||||||
|
coin.denomPub.cipher === DenomKeyType.LegacyRsa &&
|
||||||
|
contractData.wireInfoLegacyHash
|
||||||
|
) {
|
||||||
|
wireInfoHash = contractData.wireInfoLegacyHash;
|
||||||
|
} else {
|
||||||
|
wireInfoHash = contractData.wireInfoHash;
|
||||||
|
}
|
||||||
const dp = await ws.cryptoApi.signDepositPermission({
|
const dp = await ws.cryptoApi.signDepositPermission({
|
||||||
coinPriv: coin.coinPriv,
|
coinPriv: coin.coinPriv,
|
||||||
coinPub: coin.coinPub,
|
coinPub: coin.coinPub,
|
||||||
contractTermsHash: contractData.contractTermsHash,
|
contractTermsHash: contractData.contractTermsHash,
|
||||||
denomPubHash: coin.denomPubHash,
|
denomPubHash: coin.denomPubHash,
|
||||||
|
denomKeyType: coin.denomPub.cipher,
|
||||||
denomSig: coin.denomSig,
|
denomSig: coin.denomSig,
|
||||||
exchangeBaseUrl: coin.exchangeBaseUrl,
|
exchangeBaseUrl: coin.exchangeBaseUrl,
|
||||||
feeDeposit: denom.feeDeposit,
|
feeDeposit: denom.feeDeposit,
|
||||||
@ -1580,7 +1623,7 @@ export async function generateDepositPermissions(
|
|||||||
refundDeadline: contractData.refundDeadline,
|
refundDeadline: contractData.refundDeadline,
|
||||||
spendAmount: payCoinSel.coinContributions[i],
|
spendAmount: payCoinSel.coinContributions[i],
|
||||||
timestamp: contractData.timestamp,
|
timestamp: contractData.timestamp,
|
||||||
wireInfoHash: contractData.wireInfoHash,
|
wireInfoHash,
|
||||||
});
|
});
|
||||||
depositPermissions.push(dp);
|
depositPermissions.push(dp);
|
||||||
}
|
}
|
||||||
@ -1613,6 +1656,11 @@ export async function confirmPay(
|
|||||||
throw Error("proposal is in invalid state");
|
throw Error("proposal is in invalid state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const merchantInfo = await ws.merchantOps.getMerchantInfo(
|
||||||
|
ws,
|
||||||
|
d.contractData.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
const existingPurchase = await ws.db
|
const existingPurchase = await ws.db
|
||||||
.mktx((x) => ({ purchases: x.purchases }))
|
.mktx((x) => ({ purchases: x.purchases }))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
|
@ -365,7 +365,18 @@ async function refreshMelt(
|
|||||||
`coins/${oldCoin.coinPub}/melt`,
|
`coins/${oldCoin.coinPub}/melt`,
|
||||||
oldCoin.exchangeBaseUrl,
|
oldCoin.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
const meltReq = {
|
let meltReqBody: any;
|
||||||
|
if (oldCoin.denomPub.cipher === DenomKeyType.LegacyRsa) {
|
||||||
|
meltReqBody = {
|
||||||
|
coin_pub: oldCoin.coinPub,
|
||||||
|
confirm_sig: derived.confirmSig,
|
||||||
|
denom_pub_hash: oldCoin.denomPubHash,
|
||||||
|
denom_sig: oldCoin.denomSig.rsa_signature,
|
||||||
|
rc: derived.hash,
|
||||||
|
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
meltReqBody = {
|
||||||
coin_pub: oldCoin.coinPub,
|
coin_pub: oldCoin.coinPub,
|
||||||
confirm_sig: derived.confirmSig,
|
confirm_sig: derived.confirmSig,
|
||||||
denom_pub_hash: oldCoin.denomPubHash,
|
denom_pub_hash: oldCoin.denomPubHash,
|
||||||
@ -373,10 +384,10 @@ async function refreshMelt(
|
|||||||
rc: derived.hash,
|
rc: derived.hash,
|
||||||
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||||
};
|
};
|
||||||
logger.trace(`melt request for coin:`, meltReq);
|
}
|
||||||
|
|
||||||
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, meltReq, {
|
return await ws.http.postJson(reqUrl.href, meltReqBody, {
|
||||||
timeout: getRefreshRequestTimeout(refreshGroup),
|
timeout: getRefreshRequestTimeout(refreshGroup),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -604,15 +615,26 @@ async function refreshReveal(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
|
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
|
||||||
if (denom.denomPub.cipher !== 1) {
|
if (
|
||||||
|
denom.denomPub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
denom.denomPub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("cipher unsupported");
|
throw Error("cipher unsupported");
|
||||||
}
|
}
|
||||||
const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
|
const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
|
||||||
if (evSig.cipher !== DenomKeyType.Rsa) {
|
let rsaSig: string;
|
||||||
|
if (typeof evSig === "string") {
|
||||||
|
rsaSig = evSig;
|
||||||
|
} else if (
|
||||||
|
evSig.cipher === DenomKeyType.Rsa ||
|
||||||
|
evSig.cipher === DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
|
rsaSig = evSig.blinded_rsa_signature;
|
||||||
|
} else {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
|
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
|
||||||
evSig.blinded_rsa_signature,
|
rsaSig,
|
||||||
pc.blindingKey,
|
pc.blindingKey,
|
||||||
denom.denomPub.rsa_public_key,
|
denom.denomPub.rsa_public_key,
|
||||||
);
|
);
|
||||||
|
@ -314,13 +314,13 @@ async function processTipImpl(
|
|||||||
|
|
||||||
let blindedSigs: BlindedDenominationSignature[] = [];
|
let blindedSigs: BlindedDenominationSignature[] = [];
|
||||||
|
|
||||||
if (merchantInfo.supportsMerchantProtocolV2) {
|
if (merchantInfo.protocolVersionCurrent === 2) {
|
||||||
const response = await readSuccessResponseJsonOrThrow(
|
const response = await readSuccessResponseJsonOrThrow(
|
||||||
merchantResp,
|
merchantResp,
|
||||||
codecForMerchantTipResponseV2(),
|
codecForMerchantTipResponseV2(),
|
||||||
);
|
);
|
||||||
blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
|
blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
|
||||||
} else if (merchantInfo.supportsMerchantProtocolV1) {
|
} else if (merchantInfo.protocolVersionCurrent === 1) {
|
||||||
const response = await readSuccessResponseJsonOrThrow(
|
const response = await readSuccessResponseJsonOrThrow(
|
||||||
merchantResp,
|
merchantResp,
|
||||||
codecForMerchantTipResponseV1(),
|
codecForMerchantTipResponseV1(),
|
||||||
@ -347,11 +347,17 @@ async function processTipImpl(
|
|||||||
const planchet = planchets[i];
|
const planchet = planchets[i];
|
||||||
checkLogicInvariant(!!planchet);
|
checkLogicInvariant(!!planchet);
|
||||||
|
|
||||||
if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
|
if (
|
||||||
|
denom.denomPub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
denom.denomPub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blindedSig.cipher !== DenomKeyType.Rsa) {
|
if (
|
||||||
|
blindedSig.cipher !== DenomKeyType.Rsa &&
|
||||||
|
blindedSig.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
VersionMatchResult,
|
VersionMatchResult,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
LibtoolVersion,
|
LibtoolVersion,
|
||||||
|
UnblindedSignature,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
@ -591,12 +592,28 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
const { planchet, exchangeBaseUrl } = d;
|
const { planchet, exchangeBaseUrl } = d;
|
||||||
|
|
||||||
const planchetDenomPub = planchet.denomPub;
|
const planchetDenomPub = planchet.denomPub;
|
||||||
if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
|
if (
|
||||||
throw Error("cipher not supported");
|
planchetDenomPub.cipher !== DenomKeyType.Rsa &&
|
||||||
|
planchetDenomPub.cipher !== DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
|
throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const evSig = resp.ev_sig;
|
let evSig = resp.ev_sig;
|
||||||
if (evSig.cipher !== DenomKeyType.Rsa) {
|
if (typeof resp.ev_sig === "string") {
|
||||||
|
evSig = {
|
||||||
|
cipher: DenomKeyType.LegacyRsa,
|
||||||
|
blinded_rsa_signature: resp.ev_sig,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
evSig = resp.ev_sig;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
evSig.cipher === DenomKeyType.Rsa ||
|
||||||
|
evSig.cipher === DenomKeyType.LegacyRsa
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,6 +650,19 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let denomSig: UnblindedSignature;
|
||||||
|
if (
|
||||||
|
planchet.denomPub.cipher === DenomKeyType.LegacyRsa ||
|
||||||
|
planchet.denomPub.cipher === DenomKeyType.Rsa
|
||||||
|
) {
|
||||||
|
denomSig = {
|
||||||
|
cipher: planchet.denomPub.cipher,
|
||||||
|
rsa_signature: denomSigRsa,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported cipher");
|
||||||
|
}
|
||||||
|
|
||||||
const coin: CoinRecord = {
|
const coin: CoinRecord = {
|
||||||
blindingKey: planchet.blindingKey,
|
blindingKey: planchet.blindingKey,
|
||||||
coinPriv: planchet.coinPriv,
|
coinPriv: planchet.coinPriv,
|
||||||
@ -640,10 +670,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
currentAmount: planchet.coinValue,
|
currentAmount: planchet.coinValue,
|
||||||
denomPub: planchet.denomPub,
|
denomPub: planchet.denomPub,
|
||||||
denomPubHash: planchet.denomPubHash,
|
denomPubHash: planchet.denomPubHash,
|
||||||
denomSig: {
|
denomSig,
|
||||||
cipher: DenomKeyType.Rsa,
|
|
||||||
rsa_signature: denomSigRsa,
|
|
||||||
},
|
|
||||||
coinEvHash: planchet.coinEvHash,
|
coinEvHash: planchet.coinEvHash,
|
||||||
exchangeBaseUrl: exchangeBaseUrl,
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
|
@ -23,7 +23,12 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { AmountJson, Amounts, DenominationPubKey } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
DenominationPubKey,
|
||||||
|
DenomKeyType,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
import { strcmp, Logger } from "@gnu-taler/taler-util";
|
import { strcmp, Logger } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
const logger = new Logger("coinSelection.ts");
|
const logger = new Logger("coinSelection.ts");
|
||||||
@ -215,10 +220,21 @@ function denomPubCmp(
|
|||||||
} else if (p1.cipher > p2.cipher) {
|
} else if (p1.cipher > p2.cipher) {
|
||||||
return +1;
|
return +1;
|
||||||
}
|
}
|
||||||
if (p1.cipher !== 1 || p2.cipher !== 1) {
|
if (
|
||||||
throw Error("unsupported cipher");
|
p1.cipher === DenomKeyType.LegacyRsa &&
|
||||||
|
p2.cipher === DenomKeyType.LegacyRsa
|
||||||
|
) {
|
||||||
|
return strcmp(p1.rsa_public_key, p2.rsa_public_key);
|
||||||
|
} else if (p1.cipher === DenomKeyType.Rsa && p2.cipher === DenomKeyType.Rsa) {
|
||||||
|
if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
|
||||||
|
return -1;
|
||||||
|
} else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
return strcmp(p1.rsa_public_key, p2.rsa_public_key);
|
return strcmp(p1.rsa_public_key, p2.rsa_public_key);
|
||||||
|
} else {
|
||||||
|
throw Error("unsupported cipher");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,14 +19,14 @@
|
|||||||
*
|
*
|
||||||
* Uses libtool's current:revision:age versioning.
|
* Uses libtool's current:revision:age versioning.
|
||||||
*/
|
*/
|
||||||
export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:0";
|
export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protocol version spoken with the merchant.
|
* Protocol version spoken with the merchant.
|
||||||
*
|
*
|
||||||
* Uses libtool's current:revision:age versioning.
|
* Uses libtool's current:revision:age versioning.
|
||||||
*/
|
*/
|
||||||
export const WALLET_MERCHANT_PROTOCOL_VERSION = "1:0:0";
|
export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protocol version spoken with the merchant.
|
* Protocol version spoken with the merchant.
|
||||||
@ -42,4 +42,4 @@ export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
|
|||||||
*
|
*
|
||||||
* This is only a temporary measure.
|
* This is only a temporary measure.
|
||||||
*/
|
*/
|
||||||
export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "3";
|
export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "4";
|
||||||
|
@ -390,7 +390,7 @@ async function runTaskLoop(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof OperationFailedAndReportedError) {
|
if (e instanceof OperationFailedAndReportedError) {
|
||||||
logger.warn("operation processed resulted in reported error");
|
logger.warn("operation processed resulted in reported error");
|
||||||
logger.warn(`reporred error was: ${j2s(e.operationError)}`);
|
logger.warn(`reported error was: ${j2s(e.operationError)}`);
|
||||||
} else {
|
} else {
|
||||||
logger.error("Uncaught exception", e);
|
logger.error("Uncaught exception", e);
|
||||||
ws.notify({
|
ws.notify({
|
||||||
@ -985,6 +985,8 @@ export async function handleCoreApiRequest(
|
|||||||
e instanceof OperationFailedError ||
|
e instanceof OperationFailedError ||
|
||||||
e instanceof OperationFailedAndReportedError
|
e instanceof OperationFailedAndReportedError
|
||||||
) {
|
) {
|
||||||
|
logger.error("Caught operation failed error");
|
||||||
|
logger.trace((e as any).stack);
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
operation,
|
operation,
|
||||||
|
Loading…
Reference in New Issue
Block a user