wallet-core: implement accepting p2p push payments
This commit is contained in:
parent
b214934b75
commit
f11483b511
@ -227,11 +227,11 @@ async function anastasisDecrypt(
|
|||||||
const nonceBuf = ctBuf.slice(0, nonceSize);
|
const nonceBuf = ctBuf.slice(0, nonceSize);
|
||||||
const enc = ctBuf.slice(nonceSize);
|
const enc = ctBuf.slice(nonceSize);
|
||||||
const key = await deriveKey(keySeed, encodeCrock(nonceBuf), salt);
|
const key = await deriveKey(keySeed, encodeCrock(nonceBuf), salt);
|
||||||
const cipherText = secretbox_open(enc, nonceBuf, key);
|
const clearText = secretbox_open(enc, nonceBuf, key);
|
||||||
if (!cipherText) {
|
if (!clearText) {
|
||||||
throw Error("could not decrypt");
|
throw Error("could not decrypt");
|
||||||
}
|
}
|
||||||
return encodeCrock(cipherText);
|
return encodeCrock(clearText);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const asOpaque = (x: string): OpaqueData => x;
|
export const asOpaque = (x: string): OpaqueData => x;
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"big-integer": "^1.6.51",
|
"big-integer": "^1.6.51",
|
||||||
|
"fflate": "^0.7.3",
|
||||||
"jed": "^1.1.1",
|
"jed": "^1.1.1",
|
||||||
"tslib": "^2.3.1"
|
"tslib": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
@ -32,3 +32,4 @@ export {
|
|||||||
} from "./nacl-fast.js";
|
} from "./nacl-fast.js";
|
||||||
export { RequestThrottler } from "./RequestThrottler.js";
|
export { RequestThrottler } from "./RequestThrottler.js";
|
||||||
export * from "./CancellationToken.js";
|
export * from "./CancellationToken.js";
|
||||||
|
export * from "./contractTerms.js";
|
||||||
|
@ -374,7 +374,7 @@ test("taler age restriction crypto", async (t) => {
|
|||||||
const priv1 = await Edx25519.keyCreate();
|
const priv1 = await Edx25519.keyCreate();
|
||||||
const pub1 = await Edx25519.getPublic(priv1);
|
const pub1 = await Edx25519.getPublic(priv1);
|
||||||
|
|
||||||
const seed = encodeCrock(getRandomBytes(32));
|
const seed = getRandomBytes(32);
|
||||||
|
|
||||||
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
|
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
|
||||||
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
|
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
|
||||||
@ -392,18 +392,18 @@ test("edx signing", async (t) => {
|
|||||||
|
|
||||||
const sig = nacl.crypto_edx25519_sign_detached(
|
const sig = nacl.crypto_edx25519_sign_detached(
|
||||||
msg,
|
msg,
|
||||||
decodeCrock(priv1),
|
priv1,
|
||||||
decodeCrock(pub1),
|
pub1,
|
||||||
);
|
);
|
||||||
|
|
||||||
t.true(
|
t.true(
|
||||||
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
|
nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
|
||||||
);
|
);
|
||||||
|
|
||||||
sig[0]++;
|
sig[0]++;
|
||||||
|
|
||||||
t.false(
|
t.false(
|
||||||
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
|
nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -421,13 +421,19 @@ test("edx test vector", async (t) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
const pub1Prime = await Edx25519.getPublic(tv.priv1_edx);
|
const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
|
||||||
t.is(pub1Prime, tv.pub1_edx);
|
t.is(pub1Prime, decodeCrock(tv.pub1_edx));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pub2Prime = await Edx25519.publicKeyDerive(tv.pub1_edx, tv.seed);
|
const pub2Prime = await Edx25519.publicKeyDerive(
|
||||||
t.is(pub2Prime, tv.pub2_edx);
|
decodeCrock(tv.pub1_edx),
|
||||||
|
decodeCrock(tv.seed),
|
||||||
|
);
|
||||||
|
t.is(pub2Prime, decodeCrock(tv.pub2_edx));
|
||||||
|
|
||||||
const priv2Prime = await Edx25519.privateKeyDerive(tv.priv1_edx, tv.seed);
|
const priv2Prime = await Edx25519.privateKeyDerive(
|
||||||
t.is(priv2Prime, tv.priv2_edx);
|
decodeCrock(tv.priv1_edx),
|
||||||
|
decodeCrock(tv.seed),
|
||||||
|
);
|
||||||
|
t.is(priv2Prime, decodeCrock(tv.priv2_edx));
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,6 @@ import * as nacl from "./nacl-fast.js";
|
|||||||
import { kdf, kdfKw } from "./kdf.js";
|
import { kdf, kdfKw } from "./kdf.js";
|
||||||
import bigint from "big-integer";
|
import bigint from "big-integer";
|
||||||
import {
|
import {
|
||||||
Base32String,
|
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
CoinPublicKeyString,
|
CoinPublicKeyString,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
@ -33,11 +32,29 @@ import {
|
|||||||
HashCodeString,
|
HashCodeString,
|
||||||
} from "./talerTypes.js";
|
} from "./talerTypes.js";
|
||||||
import { Logger } from "./logging.js";
|
import { Logger } from "./logging.js";
|
||||||
|
import { secretbox } from "./nacl-fast.js";
|
||||||
|
import * as fflate from "fflate";
|
||||||
|
import { canonicalJson } from "./helpers.js";
|
||||||
|
|
||||||
|
export type Flavor<T, FlavorT extends string> = T & {
|
||||||
|
_flavor?: `taler.${FlavorT}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlavorP<T, FlavorT extends string, S extends number> = T & {
|
||||||
|
_flavor?: `taler.${FlavorT}`;
|
||||||
|
_size?: S;
|
||||||
|
};
|
||||||
|
|
||||||
export function getRandomBytes(n: number): Uint8Array {
|
export function getRandomBytes(n: number): Uint8Array {
|
||||||
return nacl.randomBytes(n);
|
return nacl.randomBytes(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRandomBytesF<T extends number, N extends string>(
|
||||||
|
n: T,
|
||||||
|
): FlavorP<Uint8Array, N, T> {
|
||||||
|
return nacl.randomBytes(n);
|
||||||
|
}
|
||||||
|
|
||||||
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||||
|
|
||||||
class EncodingError extends Error {
|
class EncodingError extends Error {
|
||||||
@ -157,8 +174,8 @@ export function keyExchangeEddsaEcdhe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function keyExchangeEcdheEddsa(
|
export function keyExchangeEcdheEddsa(
|
||||||
ecdhePriv: Uint8Array,
|
ecdhePriv: Uint8Array & MaterialEcdhePriv,
|
||||||
eddsaPub: Uint8Array,
|
eddsaPub: Uint8Array & MaterialEddsaPub,
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
|
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
|
||||||
const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
|
const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
|
||||||
@ -679,7 +696,8 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
|||||||
return nacl.hash(uint8ArrayBuf);
|
return nacl.hash(uint8ArrayBuf);
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
throw Error(
|
||||||
`unsupported cipher (${(pub as DenominationPubKey).cipher
|
`unsupported cipher (${
|
||||||
|
(pub as DenominationPubKey).cipher
|
||||||
}), unable to hash`,
|
}), unable to hash`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -775,6 +793,9 @@ export enum TalerSignaturePurpose {
|
|||||||
WALLET_AGE_ATTESTATION = 1207,
|
WALLET_AGE_ATTESTATION = 1207,
|
||||||
WALLET_PURSE_CREATE = 1210,
|
WALLET_PURSE_CREATE = 1210,
|
||||||
WALLET_PURSE_DEPOSIT = 1211,
|
WALLET_PURSE_DEPOSIT = 1211,
|
||||||
|
WALLET_PURSE_MERGE = 1213,
|
||||||
|
WALLET_ACCOUNT_MERGE = 1214,
|
||||||
|
WALLET_PURSE_ECONTRACT = 1216,
|
||||||
EXCHANGE_CONFIRM_RECOUP = 1039,
|
EXCHANGE_CONFIRM_RECOUP = 1039,
|
||||||
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
||||||
ANASTASIS_POLICY_UPLOAD = 1400,
|
ANASTASIS_POLICY_UPLOAD = 1400,
|
||||||
@ -782,10 +803,26 @@ export enum TalerSignaturePurpose {
|
|||||||
SYNC_BACKUP_UPLOAD = 1450,
|
SYNC_BACKUP_UPLOAD = 1450,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum WalletAccountMergeFlags {
|
||||||
|
/**
|
||||||
|
* Not a legal mode!
|
||||||
|
*/
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are merging a fully paid-up purse into a reserve.
|
||||||
|
*/
|
||||||
|
MergeFullyPaidPurse = 1,
|
||||||
|
|
||||||
|
CreateFromPurseQuota = 2,
|
||||||
|
|
||||||
|
CreateWithPurseFee = 3,
|
||||||
|
}
|
||||||
|
|
||||||
export class SignaturePurposeBuilder {
|
export class SignaturePurposeBuilder {
|
||||||
private chunks: Uint8Array[] = [];
|
private chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
constructor(private purposeNum: number) { }
|
constructor(private purposeNum: number) {}
|
||||||
|
|
||||||
put(bytes: Uint8Array): SignaturePurposeBuilder {
|
put(bytes: Uint8Array): SignaturePurposeBuilder {
|
||||||
this.chunks.push(Uint8Array.from(bytes));
|
this.chunks.push(Uint8Array.from(bytes));
|
||||||
@ -815,19 +852,10 @@ export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
|
|||||||
return new SignaturePurposeBuilder(purposeNum);
|
return new SignaturePurposeBuilder(purposeNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Flavor<T, FlavorT extends string> = T & {
|
export type OpaqueData = Flavor<Uint8Array, any>;
|
||||||
_flavor?: `taler.${FlavorT}`;
|
export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
|
||||||
};
|
export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
|
||||||
|
export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
|
||||||
export type FlavorP<T, FlavorT extends string, S extends number> = T & {
|
|
||||||
_flavor?: `taler.${FlavorT}`;
|
|
||||||
_size?: S;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpaqueData = Flavor<string, "OpaqueData">;
|
|
||||||
export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
|
|
||||||
export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
|
|
||||||
export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a big integer to a fixed-size, little-endian array.
|
* Convert a big integer to a fixed-size, little-endian array.
|
||||||
@ -859,19 +887,17 @@ export namespace Edx25519 {
|
|||||||
export async function keyCreateFromSeed(
|
export async function keyCreateFromSeed(
|
||||||
seed: OpaqueData,
|
seed: OpaqueData,
|
||||||
): Promise<Edx25519PrivateKey> {
|
): Promise<Edx25519PrivateKey> {
|
||||||
return encodeCrock(
|
return nacl.crypto_edx25519_private_key_create_from_seed(seed);
|
||||||
nacl.crypto_edx25519_private_key_create_from_seed(decodeCrock(seed)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyCreate(): Promise<Edx25519PrivateKey> {
|
export async function keyCreate(): Promise<Edx25519PrivateKey> {
|
||||||
return encodeCrock(nacl.crypto_edx25519_private_key_create());
|
return nacl.crypto_edx25519_private_key_create();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPublic(
|
export async function getPublic(
|
||||||
priv: Edx25519PrivateKey,
|
priv: Edx25519PrivateKey,
|
||||||
): Promise<Edx25519PublicKey> {
|
): Promise<Edx25519PublicKey> {
|
||||||
return encodeCrock(nacl.crypto_edx25519_get_public(decodeCrock(priv)));
|
return nacl.crypto_edx25519_get_public(priv);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sign(
|
export function sign(
|
||||||
@ -887,12 +913,12 @@ export namespace Edx25519 {
|
|||||||
): Promise<OpaqueData> {
|
): Promise<OpaqueData> {
|
||||||
const res = kdfKw({
|
const res = kdfKw({
|
||||||
outputLength: 64,
|
outputLength: 64,
|
||||||
salt: decodeCrock(seed),
|
salt: seed,
|
||||||
ikm: decodeCrock(pub),
|
ikm: pub,
|
||||||
info: stringToBytes("edx25519-derivation"),
|
info: stringToBytes("edx2559-derivation"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return encodeCrock(res);
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function privateKeyDerive(
|
export async function privateKeyDerive(
|
||||||
@ -900,21 +926,17 @@ export namespace Edx25519 {
|
|||||||
seed: OpaqueData,
|
seed: OpaqueData,
|
||||||
): Promise<Edx25519PrivateKey> {
|
): Promise<Edx25519PrivateKey> {
|
||||||
const pub = await getPublic(priv);
|
const pub = await getPublic(priv);
|
||||||
const privDec = decodeCrock(priv);
|
const privDec = priv;
|
||||||
const a = bigintFromNaclArr(privDec.subarray(0, 32));
|
const a = bigintFromNaclArr(privDec.subarray(0, 32));
|
||||||
const factorEnc = await deriveFactor(pub, seed);
|
const factorEnc = await deriveFactor(pub, seed);
|
||||||
const factorModL = bigintFromNaclArr(decodeCrock(factorEnc)).mod(L);
|
const factorModL = bigintFromNaclArr(factorEnc).mod(L);
|
||||||
|
|
||||||
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
|
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
|
||||||
const bPrime = nacl
|
const bPrime = nacl
|
||||||
.hash(
|
.hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc]))
|
||||||
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorEnc)]),
|
|
||||||
)
|
|
||||||
.subarray(0, 32);
|
.subarray(0, 32);
|
||||||
|
|
||||||
const newPriv = encodeCrock(
|
const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]);
|
||||||
typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return newPriv;
|
return newPriv;
|
||||||
}
|
}
|
||||||
@ -924,14 +946,9 @@ export namespace Edx25519 {
|
|||||||
seed: OpaqueData,
|
seed: OpaqueData,
|
||||||
): Promise<Edx25519PublicKey> {
|
): Promise<Edx25519PublicKey> {
|
||||||
const factorEnc = await deriveFactor(pub, seed);
|
const factorEnc = await deriveFactor(pub, seed);
|
||||||
const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(
|
const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc);
|
||||||
decodeCrock(factorEnc),
|
const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub);
|
||||||
);
|
return res;
|
||||||
const res = nacl.crypto_scalarmult_ed25519_noclamp(
|
|
||||||
factorReduced,
|
|
||||||
decodeCrock(pub),
|
|
||||||
);
|
|
||||||
return encodeCrock(res);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -967,7 +984,7 @@ export namespace AgeRestriction {
|
|||||||
export function hashCommitment(ac: AgeCommitment): HashCodeString {
|
export function hashCommitment(ac: AgeCommitment): HashCodeString {
|
||||||
const hc = new nacl.HashState();
|
const hc = new nacl.HashState();
|
||||||
for (const pub of ac.publicKeys) {
|
for (const pub of ac.publicKeys) {
|
||||||
hc.update(decodeCrock(pub));
|
hc.update(pub);
|
||||||
}
|
}
|
||||||
return encodeCrock(hc.finish().subarray(0, 32));
|
return encodeCrock(hc.finish().subarray(0, 32));
|
||||||
}
|
}
|
||||||
@ -1091,16 +1108,12 @@ export namespace AgeRestriction {
|
|||||||
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
|
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
|
||||||
if (group === 0) {
|
if (group === 0) {
|
||||||
// No attestation required.
|
// No attestation required.
|
||||||
return encodeCrock(new Uint8Array(64));
|
return new Uint8Array(64);
|
||||||
}
|
}
|
||||||
const priv = commitmentProof.proof.privateKeys[group - 1];
|
const priv = commitmentProof.proof.privateKeys[group - 1];
|
||||||
const pub = commitmentProof.commitment.publicKeys[group - 1];
|
const pub = commitmentProof.commitment.publicKeys[group - 1];
|
||||||
const sig = nacl.crypto_edx25519_sign_detached(
|
const sig = nacl.crypto_edx25519_sign_detached(d, priv, pub);
|
||||||
d,
|
return sig;
|
||||||
decodeCrock(priv),
|
|
||||||
decodeCrock(pub),
|
|
||||||
);
|
|
||||||
return encodeCrock(sig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commitmentVerify(
|
export function commitmentVerify(
|
||||||
@ -1118,10 +1131,138 @@ export namespace AgeRestriction {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const pub = commitment.publicKeys[group - 1];
|
const pub = commitment.publicKeys[group - 1];
|
||||||
return nacl.crypto_edx25519_sign_detached_verify(
|
return nacl.crypto_edx25519_sign_detached_verify(d, decodeCrock(sig), pub);
|
||||||
d,
|
|
||||||
decodeCrock(sig),
|
|
||||||
decodeCrock(pub),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: make it a branded type!
|
||||||
|
type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
|
||||||
|
|
||||||
|
async function deriveKey(
|
||||||
|
keySeed: OpaqueData,
|
||||||
|
nonce: EncryptionNonce,
|
||||||
|
salt: string,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
return kdfKw({
|
||||||
|
outputLength: 32,
|
||||||
|
salt: nonce,
|
||||||
|
ikm: keySeed,
|
||||||
|
info: stringToBytes(salt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptWithDerivedKey(
|
||||||
|
nonce: EncryptionNonce,
|
||||||
|
keySeed: OpaqueData,
|
||||||
|
plaintext: OpaqueData,
|
||||||
|
salt: string,
|
||||||
|
): Promise<OpaqueData> {
|
||||||
|
const key = await deriveKey(keySeed, nonce, salt);
|
||||||
|
const cipherText = secretbox(plaintext, nonce, key);
|
||||||
|
return typedArrayConcat([nonce, cipherText]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceSize = 24;
|
||||||
|
|
||||||
|
async function decryptWithDerivedKey(
|
||||||
|
ciphertext: OpaqueData,
|
||||||
|
keySeed: OpaqueData,
|
||||||
|
salt: string,
|
||||||
|
): Promise<OpaqueData> {
|
||||||
|
const ctBuf = ciphertext;
|
||||||
|
const nonceBuf = ctBuf.slice(0, nonceSize);
|
||||||
|
const enc = ctBuf.slice(nonceSize);
|
||||||
|
const key = await deriveKey(keySeed, nonceBuf, salt);
|
||||||
|
const clearText = nacl.secretbox_open(enc, nonceBuf, key);
|
||||||
|
if (!clearText) {
|
||||||
|
throw Error("could not decrypt");
|
||||||
|
}
|
||||||
|
return clearText;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContractFormatTag {
|
||||||
|
PaymentOffer = 0,
|
||||||
|
PaymentRequest = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaterialEddsaPub = {
|
||||||
|
_materialType?: "eddsa-pub";
|
||||||
|
_size?: 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MaterialEddsaPriv = {
|
||||||
|
_materialType?: "ecdhe-priv";
|
||||||
|
_size?: 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MaterialEcdhePub = {
|
||||||
|
_materialType?: "ecdhe-pub";
|
||||||
|
_size?: 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MaterialEcdhePriv = {
|
||||||
|
_materialType?: "ecdhe-priv";
|
||||||
|
_size?: 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PursePublicKey = FlavorP<Uint8Array, "PursePublicKey", 32> &
|
||||||
|
MaterialEddsaPub;
|
||||||
|
|
||||||
|
type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
|
||||||
|
MaterialEcdhePriv;
|
||||||
|
|
||||||
|
type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
|
||||||
|
MaterialEddsaPriv;
|
||||||
|
|
||||||
|
export function encryptContractForMerge(
|
||||||
|
pursePub: PursePublicKey,
|
||||||
|
contractPriv: ContractPrivateKey,
|
||||||
|
mergePriv: MergePrivateKey,
|
||||||
|
contractTerms: any,
|
||||||
|
): Promise<OpaqueData> {
|
||||||
|
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
|
||||||
|
const contractTermsBytes = stringToBytes(contractTermsCanon);
|
||||||
|
const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
|
||||||
|
const data = typedArrayConcat([
|
||||||
|
bufferForUint32(ContractFormatTag.PaymentOffer),
|
||||||
|
bufferForUint32(contractTermsBytes.length),
|
||||||
|
mergePriv,
|
||||||
|
contractTermsCompressed,
|
||||||
|
]);
|
||||||
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
|
return encryptWithDerivedKey(
|
||||||
|
getRandomBytesF(24),
|
||||||
|
key,
|
||||||
|
data,
|
||||||
|
"p2p-merge-contract",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecryptForMergeResult {
|
||||||
|
contractTerms: any;
|
||||||
|
mergePriv: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptContractForMerge(
|
||||||
|
enc: OpaqueData,
|
||||||
|
pursePub: PursePublicKey,
|
||||||
|
contractPriv: ContractPrivateKey,
|
||||||
|
): Promise<DecryptForMergeResult> {
|
||||||
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
|
const dec = await decryptWithDerivedKey(enc, key, "p2p-merge-contract");
|
||||||
|
const mergePriv = dec.slice(8, 8 + 32);
|
||||||
|
const contractTermsCompressed = dec.slice(8 + 32);
|
||||||
|
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
|
||||||
|
// Slice of the '\0' at the end and decode to a string
|
||||||
|
const contractTermsString = bytesToString(
|
||||||
|
contractTermsBuf.slice(0, contractTermsBuf.length - 1),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
mergePriv: mergePriv,
|
||||||
|
contractTerms: JSON.parse(contractTermsString),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptContractForDeposit() {
|
||||||
|
throw Error("not implemented");
|
||||||
|
}
|
||||||
|
@ -1832,3 +1832,45 @@ export interface PurseDeposit {
|
|||||||
*/
|
*/
|
||||||
coin_pub: EddsaPublicKeyString;
|
coin_pub: EddsaPublicKeyString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangePurseMergeRequest {
|
||||||
|
// payto://-URI of the account the purse is to be merged into.
|
||||||
|
// Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
|
||||||
|
payto_uri: string;
|
||||||
|
|
||||||
|
// EdDSA signature of the account/reserve affirming the merge
|
||||||
|
// over a TALER_AccountMergeSignaturePS.
|
||||||
|
// Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
|
||||||
|
reserve_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// EdDSA signature of the purse private key affirming the merge
|
||||||
|
// over a TALER_PurseMergeSignaturePS.
|
||||||
|
// Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
|
||||||
|
merge_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// Client-side timestamp of when the merge request was made.
|
||||||
|
merge_timestamp: TalerProtocolTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeGetContractResponse {
|
||||||
|
purse_pub: string;
|
||||||
|
econtract_sig: string;
|
||||||
|
econtract: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForExchangeGetContractResponse =
|
||||||
|
(): Codec<ExchangeGetContractResponse> =>
|
||||||
|
buildCodecForObject<ExchangeGetContractResponse>()
|
||||||
|
.property("purse_pub", codecForString())
|
||||||
|
.property("econtract_sig", codecForString())
|
||||||
|
.property("econtract", codecForString())
|
||||||
|
.build("ExchangeGetContractResponse");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract terms between two wallets (as opposed to a merchant and wallet).
|
||||||
|
*/
|
||||||
|
export interface PeerContractTerms {
|
||||||
|
amount: AmountString;
|
||||||
|
summary: string;
|
||||||
|
purse_expiration: TalerProtocolTimestamp;
|
||||||
|
}
|
||||||
|
@ -1263,15 +1263,50 @@ export interface PayCoinSelection {
|
|||||||
|
|
||||||
export interface InitiatePeerPushPaymentRequest {
|
export interface InitiatePeerPushPaymentRequest {
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
|
partialContractTerms: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitiatePeerPushPaymentResponse {
|
export interface InitiatePeerPushPaymentResponse {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
pursePub: string;
|
pursePub: string;
|
||||||
mergePriv: string;
|
mergePriv: string;
|
||||||
|
contractPriv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForInitiatePeerPushPaymentRequest =
|
export const codecForInitiatePeerPushPaymentRequest =
|
||||||
(): Codec<InitiatePeerPushPaymentRequest> =>
|
(): Codec<InitiatePeerPushPaymentRequest> =>
|
||||||
buildCodecForObject<InitiatePeerPushPaymentRequest>()
|
buildCodecForObject<InitiatePeerPushPaymentRequest>()
|
||||||
.property("amount", codecForAmountString())
|
.property("amount", codecForAmountString())
|
||||||
|
.property("partialContractTerms", codecForAny())
|
||||||
.build("InitiatePeerPushPaymentRequest");
|
.build("InitiatePeerPushPaymentRequest");
|
||||||
|
|
||||||
|
export interface CheckPeerPushPaymentRequest {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
pursePub: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckPeerPushPaymentResponse {
|
||||||
|
contractTerms: any;
|
||||||
|
amount: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForCheckPeerPushPaymentRequest =
|
||||||
|
(): Codec<CheckPeerPushPaymentRequest> =>
|
||||||
|
buildCodecForObject<CheckPeerPushPaymentRequest>()
|
||||||
|
.property("pursePub", codecForString())
|
||||||
|
.property("contractPriv", codecForString())
|
||||||
|
.property("exchangeBaseUrl", codecForString())
|
||||||
|
.build("CheckPeerPushPaymentRequest");
|
||||||
|
|
||||||
|
export interface AcceptPeerPushPaymentRequest {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
pursePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForAcceptPeerPushPaymentRequest =
|
||||||
|
(): Codec<AcceptPeerPushPaymentRequest> =>
|
||||||
|
buildCodecForObject<AcceptPeerPushPaymentRequest>()
|
||||||
|
.property("pursePub", codecForString())
|
||||||
|
.property("exchangeBaseUrl", codecForString())
|
||||||
|
.build("AcceptPeerPushPaymentRequest");
|
||||||
|
@ -1149,7 +1149,7 @@ testCli
|
|||||||
tVerify.start();
|
tVerify.start();
|
||||||
const attestRes = AgeRestriction.commitmentVerify(
|
const attestRes = AgeRestriction.commitmentVerify(
|
||||||
commitProof.commitment,
|
commitProof.commitment,
|
||||||
attest,
|
encodeCrock(attest),
|
||||||
18,
|
18,
|
||||||
);
|
);
|
||||||
tVerify.stop();
|
tVerify.stop();
|
||||||
@ -1157,9 +1157,12 @@ testCli
|
|||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const salt = encodeCrock(getRandomBytes(32));
|
const salt = getRandomBytes(32);
|
||||||
tDerive.start();
|
tDerive.start();
|
||||||
const deriv = await AgeRestriction.commitmentDerive(commitProof, salt);
|
const deriv = await AgeRestriction.commitmentDerive(
|
||||||
|
commitProof,
|
||||||
|
salt,
|
||||||
|
);
|
||||||
tDerive.stop();
|
tDerive.stop();
|
||||||
|
|
||||||
tCompare.start();
|
tCompare.start();
|
||||||
|
@ -44,10 +44,36 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
|
|||||||
WalletApiOperation.InitiatePeerPushPayment,
|
WalletApiOperation.InitiatePeerPushPayment,
|
||||||
{
|
{
|
||||||
amount: "TESTKUDOS:5",
|
amount: "TESTKUDOS:5",
|
||||||
|
partialContractTerms: {
|
||||||
|
summary: "Hello World",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(resp);
|
console.log(resp);
|
||||||
|
|
||||||
|
const checkResp = await wallet.client.call(
|
||||||
|
WalletApiOperation.CheckPeerPushPayment,
|
||||||
|
{
|
||||||
|
contractPriv: resp.contractPriv,
|
||||||
|
exchangeBaseUrl: resp.exchangeBaseUrl,
|
||||||
|
pursePub: resp.pursePub,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(checkResp);
|
||||||
|
|
||||||
|
const acceptResp = await wallet.client.call(
|
||||||
|
WalletApiOperation.AcceptPeerPushPayment,
|
||||||
|
{
|
||||||
|
exchangeBaseUrl: resp.exchangeBaseUrl,
|
||||||
|
pursePub: resp.pursePub,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(acceptResp);
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
runPeerToPeerTest.suites = ["wallet"];
|
runPeerToPeerTest.suites = ["wallet"];
|
||||||
|
@ -33,10 +33,12 @@ import {
|
|||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
bufferForUint32,
|
bufferForUint32,
|
||||||
buildSigPS,
|
buildSigPS,
|
||||||
|
bytesToString,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
createHashContext,
|
createHashContext,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
|
decryptContractForMerge,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
ecdheGetPublic,
|
ecdheGetPublic,
|
||||||
@ -45,6 +47,7 @@ import {
|
|||||||
eddsaSign,
|
eddsaSign,
|
||||||
eddsaVerify,
|
eddsaVerify,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
|
encryptContractForMerge,
|
||||||
ExchangeProtocolVersion,
|
ExchangeProtocolVersion,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
hash,
|
hash,
|
||||||
@ -81,10 +84,17 @@ import { DenominationRecord, WireFee } from "../db.js";
|
|||||||
import {
|
import {
|
||||||
CreateRecoupRefreshReqRequest,
|
CreateRecoupRefreshReqRequest,
|
||||||
CreateRecoupReqRequest,
|
CreateRecoupReqRequest,
|
||||||
|
DecryptContractRequest,
|
||||||
|
DecryptContractResponse,
|
||||||
DerivedRefreshSession,
|
DerivedRefreshSession,
|
||||||
DerivedTipPlanchet,
|
DerivedTipPlanchet,
|
||||||
DeriveRefreshSessionRequest,
|
DeriveRefreshSessionRequest,
|
||||||
DeriveTipRequest,
|
DeriveTipRequest,
|
||||||
|
EncryptContractRequest,
|
||||||
|
EncryptContractResponse,
|
||||||
|
EncryptedContract,
|
||||||
|
SignPurseMergeRequest,
|
||||||
|
SignPurseMergeResponse,
|
||||||
SignTrackTransactionRequest,
|
SignTrackTransactionRequest,
|
||||||
} from "./cryptoTypes.js";
|
} from "./cryptoTypes.js";
|
||||||
|
|
||||||
@ -185,6 +195,16 @@ export interface TalerCryptoInterface {
|
|||||||
signPurseDeposits(
|
signPurseDeposits(
|
||||||
req: SignPurseDepositsRequest,
|
req: SignPurseDepositsRequest,
|
||||||
): Promise<SignPurseDepositsResponse>;
|
): Promise<SignPurseDepositsResponse>;
|
||||||
|
|
||||||
|
encryptContractForMerge(
|
||||||
|
req: EncryptContractRequest,
|
||||||
|
): Promise<EncryptContractResponse>;
|
||||||
|
|
||||||
|
decryptContractForMerge(
|
||||||
|
req: DecryptContractRequest,
|
||||||
|
): Promise<DecryptContractResponse>;
|
||||||
|
|
||||||
|
signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -326,6 +346,21 @@ export const nullCrypto: TalerCryptoInterface = {
|
|||||||
): Promise<SignPurseDepositsResponse> {
|
): Promise<SignPurseDepositsResponse> {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
},
|
},
|
||||||
|
encryptContractForMerge: function (
|
||||||
|
req: EncryptContractRequest,
|
||||||
|
): Promise<EncryptContractResponse> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
decryptContractForMerge: function (
|
||||||
|
req: DecryptContractRequest,
|
||||||
|
): Promise<DecryptContractResponse> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
signPurseMerge: function (
|
||||||
|
req: SignPurseMergeRequest,
|
||||||
|
): Promise<SignPurseMergeResponse> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WithArg<X> = X extends (req: infer T) => infer R
|
export type WithArg<X> = X extends (req: infer T) => infer R
|
||||||
@ -502,6 +537,9 @@ export interface TransferPubResponse {
|
|||||||
transferPriv: string;
|
transferPriv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS-native implementation of the Taler crypto worker operations.
|
||||||
|
*/
|
||||||
export const nativeCryptoR: TalerCryptoInterfaceR = {
|
export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||||
async eddsaSign(
|
async eddsaSign(
|
||||||
tci: TalerCryptoInterfaceR,
|
tci: TalerCryptoInterfaceR,
|
||||||
@ -960,9 +998,11 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
maybeAgeCommitmentHash = ach;
|
maybeAgeCommitmentHash = ach;
|
||||||
hAgeCommitment = decodeCrock(ach);
|
hAgeCommitment = decodeCrock(ach);
|
||||||
if (depositInfo.requiredMinimumAge != null) {
|
if (depositInfo.requiredMinimumAge != null) {
|
||||||
minimumAgeSig = AgeRestriction.commitmentAttest(
|
minimumAgeSig = encodeCrock(
|
||||||
depositInfo.ageCommitmentProof,
|
AgeRestriction.commitmentAttest(
|
||||||
depositInfo.requiredMinimumAge,
|
depositInfo.ageCommitmentProof,
|
||||||
|
depositInfo.requiredMinimumAge,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1094,7 +1134,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
if (req.meltCoinAgeCommitmentProof) {
|
if (req.meltCoinAgeCommitmentProof) {
|
||||||
newAc = await AgeRestriction.commitmentDerive(
|
newAc = await AgeRestriction.commitmentDerive(
|
||||||
req.meltCoinAgeCommitmentProof,
|
req.meltCoinAgeCommitmentProof,
|
||||||
transferSecretRes.h,
|
decodeCrock(transferSecretRes.h),
|
||||||
);
|
);
|
||||||
newAch = AgeRestriction.hashCommitment(newAc.commitment);
|
newAch = AgeRestriction.hashCommitment(newAc.commitment);
|
||||||
}
|
}
|
||||||
@ -1280,6 +1320,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
for (const c of req.coins) {
|
for (const c of req.coins) {
|
||||||
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
|
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
|
||||||
.put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
|
.put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
|
||||||
|
.put(decodeCrock(c.denomPubHash))
|
||||||
|
// FIXME: use h_age_commitment here
|
||||||
|
.put(new Uint8Array(32))
|
||||||
.put(decodeCrock(req.pursePub))
|
.put(decodeCrock(req.pursePub))
|
||||||
.put(hExchangeBaseUrl)
|
.put(hExchangeBaseUrl)
|
||||||
.build();
|
.build();
|
||||||
@ -1300,6 +1343,87 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
deposits,
|
deposits,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async encryptContractForMerge(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: EncryptContractRequest,
|
||||||
|
): Promise<EncryptContractResponse> {
|
||||||
|
const contractKeyPair = await this.createEddsaKeypair(tci, {});
|
||||||
|
const enc = await encryptContractForMerge(
|
||||||
|
decodeCrock(req.pursePub),
|
||||||
|
decodeCrock(contractKeyPair.priv),
|
||||||
|
decodeCrock(req.mergePriv),
|
||||||
|
req.contractTerms,
|
||||||
|
);
|
||||||
|
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
|
||||||
|
.put(hash(enc))
|
||||||
|
.put(decodeCrock(contractKeyPair.pub))
|
||||||
|
.build();
|
||||||
|
const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
|
||||||
|
return {
|
||||||
|
econtract: {
|
||||||
|
contract_pub: contractKeyPair.pub,
|
||||||
|
econtract: encodeCrock(enc),
|
||||||
|
econtract_sig: encodeCrock(sig),
|
||||||
|
},
|
||||||
|
contractPriv: contractKeyPair.priv,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async decryptContractForMerge(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: DecryptContractRequest,
|
||||||
|
): Promise<DecryptContractResponse> {
|
||||||
|
const res = await decryptContractForMerge(
|
||||||
|
decodeCrock(req.ciphertext),
|
||||||
|
decodeCrock(req.pursePub),
|
||||||
|
decodeCrock(req.contractPriv),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
contractTerms: res.contractTerms,
|
||||||
|
mergePriv: encodeCrock(res.mergePriv),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async signPurseMerge(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: SignPurseMergeRequest,
|
||||||
|
): Promise<SignPurseMergeResponse> {
|
||||||
|
const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE)
|
||||||
|
.put(timestampRoundedToBuffer(req.mergeTimestamp))
|
||||||
|
.put(decodeCrock(req.pursePub))
|
||||||
|
.put(hashTruncate32(stringToBytes(req.reservePayto + "\0")))
|
||||||
|
.build();
|
||||||
|
const mergeSigResp = await tci.eddsaSign(tci, {
|
||||||
|
msg: encodeCrock(mergeSigBlob),
|
||||||
|
priv: req.mergePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reserveSigBlob = buildSigPS(
|
||||||
|
TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
|
||||||
|
)
|
||||||
|
.put(timestampRoundedToBuffer(req.purseExpiration))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(req.purseFee)))
|
||||||
|
.put(decodeCrock(req.contractTermsHash))
|
||||||
|
.put(decodeCrock(req.pursePub))
|
||||||
|
.put(timestampRoundedToBuffer(req.mergeTimestamp))
|
||||||
|
// FIXME: put in min_age
|
||||||
|
.put(bufferForUint32(0))
|
||||||
|
.put(bufferForUint32(req.flags))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const reserveSigResp = await tci.eddsaSign(tci, {
|
||||||
|
msg: encodeCrock(reserveSigBlob),
|
||||||
|
priv: req.reservePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mergeSig: mergeSigResp.sig,
|
||||||
|
accountSig: reserveSigResp.sig,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||||
|
@ -30,11 +30,16 @@
|
|||||||
import {
|
import {
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
|
AmountString,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
|
EddsaPublicKeyString,
|
||||||
|
EddsaSignatureString,
|
||||||
ExchangeProtocolVersion,
|
ExchangeProtocolVersion,
|
||||||
RefreshPlanchetInfo,
|
RefreshPlanchetInfo,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
|
WalletAccountMergeFlags,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export interface RefreshNewDenomInfo {
|
export interface RefreshNewDenomInfo {
|
||||||
@ -148,4 +153,80 @@ export interface CreateRecoupRefreshReqRequest {
|
|||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EncryptedContract {
|
||||||
|
/**
|
||||||
|
* Encrypted contract.
|
||||||
|
*/
|
||||||
|
econtract: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature over the (encrypted) contract.
|
||||||
|
*/
|
||||||
|
econtract_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ephemeral public key for the DH operation to decrypt the encrypted contract.
|
||||||
|
*/
|
||||||
|
contract_pub: EddsaPublicKeyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncryptContractRequest {
|
||||||
|
contractTerms: any;
|
||||||
|
|
||||||
|
pursePub: string;
|
||||||
|
pursePriv: string;
|
||||||
|
|
||||||
|
mergePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncryptContractResponse {
|
||||||
|
econtract: EncryptedContract;
|
||||||
|
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecryptContractRequest {
|
||||||
|
ciphertext: string;
|
||||||
|
pursePub: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecryptContractResponse {
|
||||||
|
contractTerms: any;
|
||||||
|
mergePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignPurseMergeRequest {
|
||||||
|
mergeTimestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
pursePub: string;
|
||||||
|
|
||||||
|
reservePayto: string;
|
||||||
|
|
||||||
|
reservePriv: string;
|
||||||
|
|
||||||
|
mergePriv: string;
|
||||||
|
|
||||||
|
purseExpiration: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
purseAmount: AmountString;
|
||||||
|
purseFee: AmountString;
|
||||||
|
|
||||||
|
contractTermsHash: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags.
|
||||||
|
*/
|
||||||
|
flags: WalletAccountMergeFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignPurseMergeResponse {
|
||||||
|
/**
|
||||||
|
* Signature made by the purse's merge private key.
|
||||||
|
*/
|
||||||
|
mergeSig: string;
|
||||||
|
|
||||||
|
accountSig: string;
|
||||||
|
}
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
TalerProtocolDuration,
|
TalerProtocolDuration,
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
|
PeerContractTerms,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { RetryInfo } from "./util/retries.js";
|
import { RetryInfo } from "./util/retries.js";
|
||||||
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
||||||
@ -561,6 +562,12 @@ export interface ExchangeRecord {
|
|||||||
* Retry status for fetching updated information about the exchange.
|
* Retry status for fetching updated information about the exchange.
|
||||||
*/
|
*/
|
||||||
retryInfo?: RetryInfo;
|
retryInfo?: RetryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key of the reserve that we're currently using for
|
||||||
|
* receiving P2P payments.
|
||||||
|
*/
|
||||||
|
currentMergeReservePub?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1675,7 +1682,6 @@ export interface BalancePerCurrencyRecord {
|
|||||||
* Record for a push P2P payment that this wallet initiated.
|
* Record for a push P2P payment that this wallet initiated.
|
||||||
*/
|
*/
|
||||||
export interface PeerPushPaymentInitiationRecord {
|
export interface PeerPushPaymentInitiationRecord {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What exchange are funds coming from?
|
* What exchange are funds coming from?
|
||||||
*/
|
*/
|
||||||
@ -1704,18 +1710,40 @@ export interface PeerPushPaymentInitiationRecord {
|
|||||||
*/
|
*/
|
||||||
mergePriv: string;
|
mergePriv: string;
|
||||||
|
|
||||||
|
contractPriv: string;
|
||||||
|
|
||||||
|
contractPub: string;
|
||||||
|
|
||||||
purseExpiration: TalerProtocolTimestamp;
|
purseExpiration: TalerProtocolTimestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Did we successfully create the purse with the exchange?
|
* Did we successfully create the purse with the exchange?
|
||||||
*/
|
*/
|
||||||
purseCreated: boolean;
|
purseCreated: boolean;
|
||||||
|
|
||||||
|
timestampCreated: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record for a push P2P payment that this wallet accepted.
|
* Record for a push P2P payment that this wallet was offered.
|
||||||
|
*
|
||||||
|
* Primary key: (exchangeBaseUrl, pursePub)
|
||||||
*/
|
*/
|
||||||
export interface PeerPushPaymentAcceptanceRecord {}
|
export interface PeerPushPaymentIncomingRecord {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
pursePub: string;
|
||||||
|
|
||||||
|
mergePriv: string;
|
||||||
|
|
||||||
|
contractPriv: string;
|
||||||
|
|
||||||
|
timestampAccepted: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
contractTerms: PeerContractTerms;
|
||||||
|
|
||||||
|
// FIXME: add status etc.
|
||||||
|
}
|
||||||
|
|
||||||
export const WalletStoresV1 = {
|
export const WalletStoresV1 = {
|
||||||
coins: describeStore(
|
coins: describeStore(
|
||||||
@ -1893,6 +1921,12 @@ export const WalletStoresV1 = {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
peerPushPaymentIncoming: describeStore(
|
||||||
|
describeContents<PeerPushPaymentIncomingRecord>("peerPushPaymentIncoming", {
|
||||||
|
keyPath: ["exchangeBaseUrl", "pursePub"],
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MetaConfigRecord {
|
export interface MetaConfigRecord {
|
||||||
|
@ -16,22 +16,46 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
|
Amounts,
|
||||||
BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms,
|
BackupCoinSourceType,
|
||||||
DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp,
|
BackupDenomSel,
|
||||||
WalletBackupContentV1
|
BackupProposalStatus,
|
||||||
|
BackupPurchase,
|
||||||
|
BackupRefreshReason,
|
||||||
|
BackupRefundState,
|
||||||
|
codecForContractTerms,
|
||||||
|
DenomKeyType,
|
||||||
|
j2s,
|
||||||
|
Logger,
|
||||||
|
PayCoinSelection,
|
||||||
|
RefreshReason,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
WalletBackupContentV1,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AbortStatus, CoinSource,
|
AbortStatus,
|
||||||
|
CoinSource,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus, DenominationVerificationStatus, DenomSelectionState, OperationStatus, ProposalDownload,
|
CoinStatus,
|
||||||
ProposalStatus, RefreshCoinStatus, RefreshSessionRecord, RefundState, ReserveBankInfo,
|
DenominationVerificationStatus,
|
||||||
ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo
|
DenomSelectionState,
|
||||||
|
OperationStatus,
|
||||||
|
ProposalDownload,
|
||||||
|
ProposalStatus,
|
||||||
|
RefreshCoinStatus,
|
||||||
|
RefreshSessionRecord,
|
||||||
|
RefundState,
|
||||||
|
ReserveBankInfo,
|
||||||
|
ReserveRecordStatus,
|
||||||
|
WalletContractData,
|
||||||
|
WalletRefundItem,
|
||||||
|
WalletStoresV1,
|
||||||
|
WireInfo,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
import {
|
import {
|
||||||
checkDbInvariant,
|
checkDbInvariant,
|
||||||
checkLogicInvariant
|
checkLogicInvariant,
|
||||||
} from "../../util/invariants.js";
|
} from "../../util/invariants.js";
|
||||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||||
import { RetryInfo } from "../../util/retries.js";
|
import { RetryInfo } from "../../util/retries.js";
|
||||||
@ -313,14 +337,12 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const backupDenomination of backupExchangeDetails.denominations) {
|
for (const backupDenomination of backupExchangeDetails.denominations) {
|
||||||
if (
|
if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
|
||||||
backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa
|
|
||||||
) {
|
|
||||||
throw Error("unsupported cipher");
|
throw Error("unsupported cipher");
|
||||||
}
|
}
|
||||||
const denomPubHash =
|
const denomPubHash =
|
||||||
cryptoComp.rsaDenomPubToHash[
|
cryptoComp.rsaDenomPubToHash[
|
||||||
backupDenomination.denom_pub.rsa_public_key
|
backupDenomination.denom_pub.rsa_public_key
|
||||||
];
|
];
|
||||||
checkLogicInvariant(!!denomPubHash);
|
checkLogicInvariant(!!denomPubHash);
|
||||||
const existingDenom = await tx.denominations.get([
|
const existingDenom = await tx.denominations.get([
|
||||||
@ -535,7 +557,7 @@ export async function importBackup(
|
|||||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
const contractTermsHash =
|
const contractTermsHash =
|
||||||
cryptoComp.proposalIdToContractTermsHash[
|
cryptoComp.proposalIdToContractTermsHash[
|
||||||
backupProposal.proposal_id
|
backupProposal.proposal_id
|
||||||
];
|
];
|
||||||
let maxWireFee: AmountJson;
|
let maxWireFee: AmountJson;
|
||||||
if (parsedContractTerms.max_wire_fee) {
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
@ -679,7 +701,7 @@ export async function importBackup(
|
|||||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
const contractTermsHash =
|
const contractTermsHash =
|
||||||
cryptoComp.proposalIdToContractTermsHash[
|
cryptoComp.proposalIdToContractTermsHash[
|
||||||
backupPurchase.proposal_id
|
backupPurchase.proposal_id
|
||||||
];
|
];
|
||||||
let maxWireFee: AmountJson;
|
let maxWireFee: AmountJson;
|
||||||
if (parsedContractTerms.max_wire_fee) {
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
ConfirmPayResultType,
|
ConfirmPayResultType,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
|
ContractTermsUtil,
|
||||||
Duration,
|
Duration,
|
||||||
durationMax,
|
durationMax,
|
||||||
durationMin,
|
durationMin,
|
||||||
@ -87,7 +88,6 @@ import {
|
|||||||
selectForcedPayCoins,
|
selectForcedPayCoins,
|
||||||
selectPayCoins,
|
selectPayCoins,
|
||||||
} from "../util/coinSelection.js";
|
} from "../util/coinSelection.js";
|
||||||
import { ContractTermsUtil } from "../util/contractTerms.js";
|
|
||||||
import {
|
import {
|
||||||
getHttpResponseErrorDetails,
|
getHttpResponseErrorDetails,
|
||||||
readSuccessResponseJsonOrErrorCode,
|
readSuccessResponseJsonOrErrorCode,
|
||||||
|
@ -18,25 +18,47 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AcceptPeerPushPaymentRequest,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
Logger,
|
|
||||||
InitiatePeerPushPaymentResponse,
|
|
||||||
InitiatePeerPushPaymentRequest,
|
|
||||||
strcmp,
|
|
||||||
CoinPublicKeyString,
|
|
||||||
j2s,
|
|
||||||
getRandomBytes,
|
|
||||||
Duration,
|
|
||||||
durationAdd,
|
|
||||||
TalerProtocolTimestamp,
|
|
||||||
AbsoluteTime,
|
|
||||||
encodeCrock,
|
|
||||||
AmountString,
|
AmountString,
|
||||||
|
buildCodecForObject,
|
||||||
|
CheckPeerPushPaymentRequest,
|
||||||
|
CheckPeerPushPaymentResponse,
|
||||||
|
Codec,
|
||||||
|
codecForAmountString,
|
||||||
|
codecForAny,
|
||||||
|
codecForExchangeGetContractResponse,
|
||||||
|
ContractTermsUtil,
|
||||||
|
decodeCrock,
|
||||||
|
Duration,
|
||||||
|
eddsaGetPublic,
|
||||||
|
encodeCrock,
|
||||||
|
ExchangePurseMergeRequest,
|
||||||
|
InitiatePeerPushPaymentRequest,
|
||||||
|
InitiatePeerPushPaymentResponse,
|
||||||
|
j2s,
|
||||||
|
Logger,
|
||||||
|
strcmp,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
|
WalletAccountMergeFlags,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { CoinStatus } from "../db.js";
|
import { url } from "inspector";
|
||||||
|
import {
|
||||||
|
CoinStatus,
|
||||||
|
OperationStatus,
|
||||||
|
ReserveRecord,
|
||||||
|
ReserveRecordStatus,
|
||||||
|
} from "../db.js";
|
||||||
|
import {
|
||||||
|
checkSuccessResponseOrThrow,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
throwUnexpectedRequestError,
|
||||||
|
} from "../util/http.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/peer-to-peer.ts");
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
@ -176,14 +198,22 @@ export async function initiatePeerToPeerPush(
|
|||||||
|
|
||||||
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
const hContractTerms = encodeCrock(getRandomBytes(64));
|
|
||||||
const purseExpiration = AbsoluteTime.toTimestamp(
|
const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
|
||||||
AbsoluteTime.addDuration(
|
AbsoluteTime.addDuration(
|
||||||
AbsoluteTime.now(),
|
AbsoluteTime.now(),
|
||||||
Duration.fromSpec({ days: 2 }),
|
Duration.fromSpec({ days: 2 }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const contractTerms = {
|
||||||
|
...req.partialContractTerms,
|
||||||
|
purse_expiration: purseExpiration,
|
||||||
|
amount: req.amount,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
||||||
|
|
||||||
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
||||||
hContractTerms,
|
hContractTerms,
|
||||||
mergePub: mergePair.pub,
|
mergePub: mergePair.pub,
|
||||||
@ -204,6 +234,13 @@ export async function initiatePeerToPeerPush(
|
|||||||
coinSelRes.exchangeBaseUrl,
|
coinSelRes.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const econtractResp = await ws.cryptoApi.encryptContractForMerge({
|
||||||
|
contractTerms,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
|
||||||
const httpResp = await ws.http.postJson(createPurseUrl.href, {
|
const httpResp = await ws.http.postJson(createPurseUrl.href, {
|
||||||
amount: Amounts.stringify(instructedAmount),
|
amount: Amounts.stringify(instructedAmount),
|
||||||
merge_pub: mergePair.pub,
|
merge_pub: mergePair.pub,
|
||||||
@ -212,11 +249,216 @@ export async function initiatePeerToPeerPush(
|
|||||||
purse_expiration: purseExpiration,
|
purse_expiration: purseExpiration,
|
||||||
deposits: depositSigsResp.deposits,
|
deposits: depositSigsResp.deposits,
|
||||||
min_age: 0,
|
min_age: 0,
|
||||||
|
econtract: econtractResp.econtract,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resp = await httpResp.json();
|
const resp = await httpResp.json();
|
||||||
|
|
||||||
logger.info(`resp: ${j2s(resp)}`);
|
logger.info(`resp: ${j2s(resp)}`);
|
||||||
|
|
||||||
throw Error("not yet implemented");
|
if (httpResp.status !== 200) {
|
||||||
|
throw Error("got error response from exchange");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractPriv: econtractResp.contractPriv,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangePurseStatus {
|
||||||
|
balance: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
|
||||||
|
buildCodecForObject<ExchangePurseStatus>()
|
||||||
|
.property("balance", codecForAmountString())
|
||||||
|
.build("ExchangePurseStatus");
|
||||||
|
|
||||||
|
export async function checkPeerPushPayment(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: CheckPeerPushPaymentRequest,
|
||||||
|
): Promise<CheckPeerPushPaymentResponse> {
|
||||||
|
const getPurseUrl = new URL(
|
||||||
|
`purses/${req.pursePub}/deposit`,
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractPub = encodeCrock(
|
||||||
|
eddsaGetPublic(decodeCrock(req.contractPriv)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const purseHttpResp = await ws.http.get(getPurseUrl.href);
|
||||||
|
|
||||||
|
const purseStatus = await readSuccessResponseJsonOrThrow(
|
||||||
|
purseHttpResp,
|
||||||
|
codecForExchangePurseStatus(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const getContractUrl = new URL(
|
||||||
|
`contracts/${contractPub}`,
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractHttpResp = await ws.http.get(getContractUrl.href);
|
||||||
|
|
||||||
|
const contractResp = await readSuccessResponseJsonOrThrow(
|
||||||
|
contractHttpResp,
|
||||||
|
codecForExchangeGetContractResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dec = await ws.cryptoApi.decryptContractForMerge({
|
||||||
|
ciphertext: contractResp.econtract,
|
||||||
|
contractPriv: req.contractPriv,
|
||||||
|
pursePub: req.pursePub,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
peerPushPaymentIncoming: x.peerPushPaymentIncoming,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.peerPushPaymentIncoming.add({
|
||||||
|
contractPriv: req.contractPriv,
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
mergePriv: dec.mergePriv,
|
||||||
|
pursePub: req.pursePub,
|
||||||
|
timestampAccepted: TalerProtocolTimestamp.now(),
|
||||||
|
contractTerms: dec.contractTerms,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: purseStatus.balance,
|
||||||
|
contractTerms: dec.contractTerms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function talerPaytoFromExchangeReserve(
|
||||||
|
exchangeBaseUrl: string,
|
||||||
|
reservePub: string,
|
||||||
|
): string {
|
||||||
|
const url = new URL(exchangeBaseUrl);
|
||||||
|
let proto: string;
|
||||||
|
if (url.protocol === "http:") {
|
||||||
|
proto = "taler+http";
|
||||||
|
} else if (url.protocol === "https:") {
|
||||||
|
proto = "taler";
|
||||||
|
} else {
|
||||||
|
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = url.pathname;
|
||||||
|
if (!path.endsWith("/")) {
|
||||||
|
path = path + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acceptPeerPushPayment(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: AcceptPeerPushPaymentRequest,
|
||||||
|
) {
|
||||||
|
const peerInc = await ws.db
|
||||||
|
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.peerPushPaymentIncoming.get([
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
req.pursePub,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!peerInc) {
|
||||||
|
throw Error("can't accept unknown incoming p2p push payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
|
||||||
|
|
||||||
|
// We have to create the key pair outside of the transaction,
|
||||||
|
// due to the async crypto API.
|
||||||
|
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const reserve: ReserveRecord | undefined = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
|
||||||
|
checkDbInvariant(!!ex);
|
||||||
|
if (ex.currentMergeReservePub) {
|
||||||
|
return await tx.reserves.get(ex.currentMergeReservePub);
|
||||||
|
}
|
||||||
|
const rec: ReserveRecord = {
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
// FIXME: field will be removed in the future, folded into withdrawal/p2p record.
|
||||||
|
reserveStatus: ReserveRecordStatus.Dormant,
|
||||||
|
timestampCreated: TalerProtocolTimestamp.now(),
|
||||||
|
instructedAmount: Amounts.getZero(amount.currency),
|
||||||
|
currency: amount.currency,
|
||||||
|
reservePub: newReservePair.pub,
|
||||||
|
reservePriv: newReservePair.priv,
|
||||||
|
timestampBankConfirmed: undefined,
|
||||||
|
timestampReserveInfoPosted: undefined,
|
||||||
|
// FIXME!
|
||||||
|
initialDenomSel: undefined as any,
|
||||||
|
// FIXME!
|
||||||
|
initialWithdrawalGroupId: "",
|
||||||
|
initialWithdrawalStarted: false,
|
||||||
|
lastError: undefined,
|
||||||
|
operationStatus: OperationStatus.Pending,
|
||||||
|
retryInfo: undefined,
|
||||||
|
bankInfo: undefined,
|
||||||
|
restrictAge: undefined,
|
||||||
|
senderWire: undefined,
|
||||||
|
};
|
||||||
|
await tx.reserves.put(rec);
|
||||||
|
return rec;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!reserve) {
|
||||||
|
throw Error("can't create reserve");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeTimestamp = TalerProtocolTimestamp.now();
|
||||||
|
|
||||||
|
const reservePayto = talerPaytoFromExchangeReserve(
|
||||||
|
reserve.exchangeBaseUrl,
|
||||||
|
reserve.reservePub,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sigRes = await ws.cryptoApi.signPurseMerge({
|
||||||
|
contractTermsHash: ContractTermsUtil.hashContractTerms(
|
||||||
|
peerInc.contractTerms,
|
||||||
|
),
|
||||||
|
flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
|
||||||
|
mergePriv: peerInc.mergePriv,
|
||||||
|
mergeTimestamp: mergeTimestamp,
|
||||||
|
purseAmount: Amounts.stringify(amount),
|
||||||
|
purseExpiration: peerInc.contractTerms.purse_expiration,
|
||||||
|
purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
|
||||||
|
pursePub: peerInc.pursePub,
|
||||||
|
reservePayto,
|
||||||
|
reservePriv: reserve.reservePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergePurseUrl = new URL(
|
||||||
|
`purses/${req.pursePub}/merge`,
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergeReq: ExchangePurseMergeRequest = {
|
||||||
|
payto_uri: reservePayto,
|
||||||
|
merge_timestamp: mergeTimestamp,
|
||||||
|
merge_sig: sigRes.mergeSig,
|
||||||
|
reserve_sig: sigRes.accountSig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
||||||
|
|
||||||
|
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
||||||
|
logger.info(`merge result: ${j2s(res)}`);
|
||||||
}
|
}
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports.
|
|
||||||
*/
|
|
||||||
import test from "ava";
|
|
||||||
import { ContractTermsUtil } from "./contractTerms.js";
|
|
||||||
|
|
||||||
test("contract terms canon hashing", (t) => {
|
|
||||||
const cReq = {
|
|
||||||
foo: 42,
|
|
||||||
bar: "hello",
|
|
||||||
$forgettable: {
|
|
||||||
foo: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const c1 = ContractTermsUtil.saltForgettable(cReq);
|
|
||||||
const c2 = ContractTermsUtil.saltForgettable(cReq);
|
|
||||||
t.assert(typeof cReq.$forgettable.foo === "boolean");
|
|
||||||
t.assert(typeof c1.$forgettable.foo === "string");
|
|
||||||
t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
|
|
||||||
|
|
||||||
const h1 = ContractTermsUtil.hashContractTerms(c1);
|
|
||||||
|
|
||||||
const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
|
|
||||||
|
|
||||||
t.assert(c3.foo === undefined);
|
|
||||||
t.assert(c3.bar === cReq.bar);
|
|
||||||
|
|
||||||
const h2 = ContractTermsUtil.hashContractTerms(c3);
|
|
||||||
|
|
||||||
t.deepEqual(h1, h2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("contract terms canon hashing (nested)", (t) => {
|
|
||||||
const cReq = {
|
|
||||||
foo: 42,
|
|
||||||
bar: {
|
|
||||||
prop1: "hello, world",
|
|
||||||
$forgettable: {
|
|
||||||
prop1: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
$forgettable: {
|
|
||||||
bar: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const c1 = ContractTermsUtil.saltForgettable(cReq);
|
|
||||||
|
|
||||||
t.is(typeof c1.$forgettable.bar, "string");
|
|
||||||
t.is(typeof c1.bar.$forgettable.prop1, "string");
|
|
||||||
|
|
||||||
const forgetPath = (x: any, s: string) =>
|
|
||||||
ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
|
|
||||||
|
|
||||||
// Forget bar first
|
|
||||||
const c2 = forgetPath(c1, "bar");
|
|
||||||
|
|
||||||
// Forget bar.prop1 first
|
|
||||||
const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
|
|
||||||
|
|
||||||
// Forget everything
|
|
||||||
const c4 = ContractTermsUtil.scrub(c1);
|
|
||||||
|
|
||||||
const h1 = ContractTermsUtil.hashContractTerms(c1);
|
|
||||||
const h2 = ContractTermsUtil.hashContractTerms(c2);
|
|
||||||
const h3 = ContractTermsUtil.hashContractTerms(c3);
|
|
||||||
const h4 = ContractTermsUtil.hashContractTerms(c4);
|
|
||||||
|
|
||||||
t.is(h1, h2);
|
|
||||||
t.is(h1, h3);
|
|
||||||
t.is(h1, h4);
|
|
||||||
|
|
||||||
// Doesn't contain salt
|
|
||||||
t.false(ContractTermsUtil.validateForgettable(cReq));
|
|
||||||
|
|
||||||
t.true(ContractTermsUtil.validateForgettable(c1));
|
|
||||||
t.true(ContractTermsUtil.validateForgettable(c2));
|
|
||||||
t.true(ContractTermsUtil.validateForgettable(c3));
|
|
||||||
t.true(ContractTermsUtil.validateForgettable(c4));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("contract terms reference vector", (t) => {
|
|
||||||
const j = {
|
|
||||||
k1: 1,
|
|
||||||
$forgettable: {
|
|
||||||
k1: "SALT",
|
|
||||||
},
|
|
||||||
k2: {
|
|
||||||
n1: true,
|
|
||||||
$forgettable: {
|
|
||||||
n1: "salt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
k3: {
|
|
||||||
n1: "string",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const h = ContractTermsUtil.hashContractTerms(j);
|
|
||||||
|
|
||||||
t.deepEqual(
|
|
||||||
h,
|
|
||||||
"VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,230 +0,0 @@
|
|||||||
/*
|
|
||||||
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/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { canonicalJson, Logger } from "@gnu-taler/taler-util";
|
|
||||||
import { kdf } from "@gnu-taler/taler-util";
|
|
||||||
import {
|
|
||||||
decodeCrock,
|
|
||||||
encodeCrock,
|
|
||||||
getRandomBytes,
|
|
||||||
hash,
|
|
||||||
stringToBytes,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
|
|
||||||
const logger = new Logger("contractTerms.ts");
|
|
||||||
|
|
||||||
export namespace ContractTermsUtil {
|
|
||||||
export type PathPredicate = (path: string[]) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scrub all forgettable members from an object.
|
|
||||||
*/
|
|
||||||
export function scrub(anyJson: any): any {
|
|
||||||
return forgetAllImpl(anyJson, [], () => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively forget all forgettable members of an object,
|
|
||||||
* where the path matches a predicate.
|
|
||||||
*/
|
|
||||||
export function forgetAll(anyJson: any, pred: PathPredicate): any {
|
|
||||||
return forgetAllImpl(anyJson, [], pred);
|
|
||||||
}
|
|
||||||
|
|
||||||
function forgetAllImpl(
|
|
||||||
anyJson: any,
|
|
||||||
path: string[],
|
|
||||||
pred: PathPredicate,
|
|
||||||
): any {
|
|
||||||
const dup = JSON.parse(JSON.stringify(anyJson));
|
|
||||||
if (Array.isArray(dup)) {
|
|
||||||
for (let i = 0; i < dup.length; i++) {
|
|
||||||
dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
|
|
||||||
}
|
|
||||||
} else if (typeof dup === "object" && dup != null) {
|
|
||||||
if (typeof dup.$forgettable === "object") {
|
|
||||||
for (const x of Object.keys(dup.$forgettable)) {
|
|
||||||
if (!pred([...path, x])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!dup.$forgotten) {
|
|
||||||
dup.$forgotten = {};
|
|
||||||
}
|
|
||||||
if (!dup.$forgotten[x]) {
|
|
||||||
const membValCanon = stringToBytes(
|
|
||||||
canonicalJson(scrub(dup[x])) + "\0",
|
|
||||||
);
|
|
||||||
const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
|
|
||||||
const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
|
|
||||||
dup.$forgotten[x] = encodeCrock(h);
|
|
||||||
}
|
|
||||||
delete dup[x];
|
|
||||||
delete dup.$forgettable[x];
|
|
||||||
}
|
|
||||||
if (Object.keys(dup.$forgettable).length === 0) {
|
|
||||||
delete dup.$forgettable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const x of Object.keys(dup)) {
|
|
||||||
if (x.startsWith("$")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a salt for all members marked as forgettable,
|
|
||||||
* but which don't have an actual salt yet.
|
|
||||||
*/
|
|
||||||
export function saltForgettable(anyJson: any): any {
|
|
||||||
const dup = JSON.parse(JSON.stringify(anyJson));
|
|
||||||
if (Array.isArray(dup)) {
|
|
||||||
for (let i = 0; i < dup.length; i++) {
|
|
||||||
dup[i] = saltForgettable(dup[i]);
|
|
||||||
}
|
|
||||||
} else if (typeof dup === "object" && dup !== null) {
|
|
||||||
if (typeof dup.$forgettable === "object") {
|
|
||||||
for (const k of Object.keys(dup.$forgettable)) {
|
|
||||||
if (dup.$forgettable[k] === true) {
|
|
||||||
dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const x of Object.keys(dup)) {
|
|
||||||
if (x.startsWith("$")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
dup[x] = saltForgettable(dup[x]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameRegex = /^[0-9A-Za-z_]+$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that the given JSON object is well-formed with regards
|
|
||||||
* to forgettable fields and other restrictions for forgettable JSON.
|
|
||||||
*/
|
|
||||||
export function validateForgettable(anyJson: any): boolean {
|
|
||||||
if (typeof anyJson === "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeof anyJson === "number") {
|
|
||||||
return (
|
|
||||||
Number.isInteger(anyJson) &&
|
|
||||||
anyJson >= Number.MIN_SAFE_INTEGER &&
|
|
||||||
anyJson <= Number.MAX_SAFE_INTEGER
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof anyJson === "boolean") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (anyJson === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (Array.isArray(anyJson)) {
|
|
||||||
return anyJson.every((x) => validateForgettable(x));
|
|
||||||
}
|
|
||||||
if (typeof anyJson === "object") {
|
|
||||||
for (const k of Object.keys(anyJson)) {
|
|
||||||
if (k.match(nameRegex)) {
|
|
||||||
if (validateForgettable(anyJson[k])) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (k === "$forgettable") {
|
|
||||||
const fga = anyJson.$forgettable;
|
|
||||||
if (!fga || typeof fga !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const fk of Object.keys(fga)) {
|
|
||||||
if (!fk.match(nameRegex)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(fk in anyJson)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const fv = anyJson.$forgettable[fk];
|
|
||||||
if (typeof fv !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (k === "$forgotten") {
|
|
||||||
const fgo = anyJson.$forgotten;
|
|
||||||
if (!fgo || typeof fgo !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const fk of Object.keys(fgo)) {
|
|
||||||
if (!fk.match(nameRegex)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Check that the value has actually been forgotten.
|
|
||||||
if (fk in anyJson) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const fv = anyJson.$forgotten[fk];
|
|
||||||
if (typeof fv !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const decFv = decodeCrock(fv);
|
|
||||||
if (decFv.length != 64) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Check that salt has been deleted after forgetting.
|
|
||||||
if (anyJson.$forgettable?.[k] !== undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that no forgettable information has been forgotten.
|
|
||||||
*
|
|
||||||
* Must only be called on an object already validated with validateForgettable.
|
|
||||||
*/
|
|
||||||
export function validateNothingForgotten(contractTerms: any): boolean {
|
|
||||||
throw Error("not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash a contract terms object. Forgettable fields
|
|
||||||
* are scrubbed and JSON canonicalization is applied
|
|
||||||
* before hashing.
|
|
||||||
*/
|
|
||||||
export function hashContractTerms(contractTerms: unknown): string {
|
|
||||||
const cleaned = scrub(contractTerms);
|
|
||||||
const canon = canonicalJson(cleaned) + "\0";
|
|
||||||
const bytes = stringToBytes(canon);
|
|
||||||
return encodeCrock(hash(bytes));
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,6 +27,7 @@ import {
|
|||||||
AcceptExchangeTosRequest,
|
AcceptExchangeTosRequest,
|
||||||
AcceptManualWithdrawalRequest,
|
AcceptManualWithdrawalRequest,
|
||||||
AcceptManualWithdrawalResult,
|
AcceptManualWithdrawalResult,
|
||||||
|
AcceptPeerPushPaymentRequest,
|
||||||
AcceptTipRequest,
|
AcceptTipRequest,
|
||||||
AcceptWithdrawalResponse,
|
AcceptWithdrawalResponse,
|
||||||
AddExchangeRequest,
|
AddExchangeRequest,
|
||||||
@ -34,6 +35,8 @@ import {
|
|||||||
ApplyRefundResponse,
|
ApplyRefundResponse,
|
||||||
BackupRecovery,
|
BackupRecovery,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
|
CheckPeerPushPaymentRequest,
|
||||||
|
CheckPeerPushPaymentResponse,
|
||||||
CoinDumpJson,
|
CoinDumpJson,
|
||||||
ConfirmPayRequest,
|
ConfirmPayRequest,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
@ -286,6 +289,14 @@ export type WalletOperations = {
|
|||||||
request: InitiatePeerPushPaymentRequest;
|
request: InitiatePeerPushPaymentRequest;
|
||||||
response: InitiatePeerPushPaymentResponse;
|
response: InitiatePeerPushPaymentResponse;
|
||||||
};
|
};
|
||||||
|
[WalletApiOperation.CheckPeerPushPayment]: {
|
||||||
|
request: CheckPeerPushPaymentRequest;
|
||||||
|
response: CheckPeerPushPaymentResponse;
|
||||||
|
};
|
||||||
|
[WalletApiOperation.AcceptPeerPushPayment]: {
|
||||||
|
request: AcceptPeerPushPaymentRequest;
|
||||||
|
response: {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestType<
|
export type RequestType<
|
||||||
|
@ -32,11 +32,13 @@ import {
|
|||||||
codecForAcceptBankIntegratedWithdrawalRequest,
|
codecForAcceptBankIntegratedWithdrawalRequest,
|
||||||
codecForAcceptExchangeTosRequest,
|
codecForAcceptExchangeTosRequest,
|
||||||
codecForAcceptManualWithdrawalRequet,
|
codecForAcceptManualWithdrawalRequet,
|
||||||
|
codecForAcceptPeerPushPaymentRequest,
|
||||||
codecForAcceptTipRequest,
|
codecForAcceptTipRequest,
|
||||||
codecForAddExchangeRequest,
|
codecForAddExchangeRequest,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
codecForApplyRefundFromPurchaseIdRequest,
|
codecForApplyRefundFromPurchaseIdRequest,
|
||||||
codecForApplyRefundRequest,
|
codecForApplyRefundRequest,
|
||||||
|
codecForCheckPeerPushPaymentRequest,
|
||||||
codecForConfirmPayRequest,
|
codecForConfirmPayRequest,
|
||||||
codecForCreateDepositGroupRequest,
|
codecForCreateDepositGroupRequest,
|
||||||
codecForDeleteTransactionRequest,
|
codecForDeleteTransactionRequest,
|
||||||
@ -144,7 +146,11 @@ import {
|
|||||||
processDownloadProposal,
|
processDownloadProposal,
|
||||||
processPurchasePay,
|
processPurchasePay,
|
||||||
} from "./operations/pay.js";
|
} from "./operations/pay.js";
|
||||||
import { initiatePeerToPeerPush } from "./operations/peer-to-peer.js";
|
import {
|
||||||
|
acceptPeerPushPayment,
|
||||||
|
checkPeerPushPayment,
|
||||||
|
initiatePeerToPeerPush,
|
||||||
|
} from "./operations/peer-to-peer.js";
|
||||||
import { getPendingOperations } from "./operations/pending.js";
|
import { getPendingOperations } from "./operations/pending.js";
|
||||||
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
|
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
|
||||||
import {
|
import {
|
||||||
@ -1055,6 +1061,15 @@ async function dispatchRequestInternal(
|
|||||||
const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
|
const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
|
||||||
return await initiatePeerToPeerPush(ws, req);
|
return await initiatePeerToPeerPush(ws, req);
|
||||||
}
|
}
|
||||||
|
case "checkPeerPushPayment": {
|
||||||
|
const req = codecForCheckPeerPushPaymentRequest().decode(payload);
|
||||||
|
return await checkPeerPushPayment(ws, req);
|
||||||
|
}
|
||||||
|
case "acceptPeerPushPayment": {
|
||||||
|
const req = codecForAcceptPeerPushPaymentRequest().decode(payload);
|
||||||
|
await acceptPeerPushPayment(ws, req);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
||||||
|
@ -169,6 +169,7 @@ importers:
|
|||||||
ava: ^4.0.1
|
ava: ^4.0.1
|
||||||
big-integer: ^1.6.51
|
big-integer: ^1.6.51
|
||||||
esbuild: ^0.14.21
|
esbuild: ^0.14.21
|
||||||
|
fflate: ^0.7.3
|
||||||
jed: ^1.1.1
|
jed: ^1.1.1
|
||||||
prettier: ^2.5.1
|
prettier: ^2.5.1
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
@ -176,6 +177,7 @@ importers:
|
|||||||
typescript: ^4.5.5
|
typescript: ^4.5.5
|
||||||
dependencies:
|
dependencies:
|
||||||
big-integer: 1.6.51
|
big-integer: 1.6.51
|
||||||
|
fflate: 0.7.3
|
||||||
jed: 1.1.1
|
jed: 1.1.1
|
||||||
tslib: 2.3.1
|
tslib: 2.3.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user