peer-to-peer pull payments MVP
p2p pull wip
This commit is contained in:
parent
4ca38113ab
commit
f3ff5a7225
@ -1214,6 +1214,9 @@ type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
|
|||||||
type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
|
type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
|
||||||
MaterialEddsaPriv;
|
MaterialEddsaPriv;
|
||||||
|
|
||||||
|
const mergeSalt = "p2p-merge-contract";
|
||||||
|
const depositSalt = "p2p-deposit-contract";
|
||||||
|
|
||||||
export function encryptContractForMerge(
|
export function encryptContractForMerge(
|
||||||
pursePub: PursePublicKey,
|
pursePub: PursePublicKey,
|
||||||
contractPriv: ContractPrivateKey,
|
contractPriv: ContractPrivateKey,
|
||||||
@ -1230,12 +1233,24 @@ export function encryptContractForMerge(
|
|||||||
contractTermsCompressed,
|
contractTermsCompressed,
|
||||||
]);
|
]);
|
||||||
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
return encryptWithDerivedKey(
|
return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
|
||||||
getRandomBytesF(24),
|
}
|
||||||
key,
|
|
||||||
data,
|
export function encryptContractForDeposit(
|
||||||
"p2p-merge-contract",
|
pursePub: PursePublicKey,
|
||||||
);
|
contractPriv: ContractPrivateKey,
|
||||||
|
contractTerms: any,
|
||||||
|
): Promise<OpaqueData> {
|
||||||
|
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
|
||||||
|
const contractTermsBytes = stringToBytes(contractTermsCanon);
|
||||||
|
const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
|
||||||
|
const data = typedArrayConcat([
|
||||||
|
bufferForUint32(ContractFormatTag.PaymentRequest),
|
||||||
|
bufferForUint32(contractTermsBytes.length),
|
||||||
|
contractTermsCompressed,
|
||||||
|
]);
|
||||||
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
|
return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DecryptForMergeResult {
|
export interface DecryptForMergeResult {
|
||||||
@ -1243,13 +1258,17 @@ export interface DecryptForMergeResult {
|
|||||||
mergePriv: Uint8Array;
|
mergePriv: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DecryptForDepositResult {
|
||||||
|
contractTerms: any;
|
||||||
|
}
|
||||||
|
|
||||||
export async function decryptContractForMerge(
|
export async function decryptContractForMerge(
|
||||||
enc: OpaqueData,
|
enc: OpaqueData,
|
||||||
pursePub: PursePublicKey,
|
pursePub: PursePublicKey,
|
||||||
contractPriv: ContractPrivateKey,
|
contractPriv: ContractPrivateKey,
|
||||||
): Promise<DecryptForMergeResult> {
|
): Promise<DecryptForMergeResult> {
|
||||||
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
const dec = await decryptWithDerivedKey(enc, key, "p2p-merge-contract");
|
const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
|
||||||
const mergePriv = dec.slice(8, 8 + 32);
|
const mergePriv = dec.slice(8, 8 + 32);
|
||||||
const contractTermsCompressed = dec.slice(8 + 32);
|
const contractTermsCompressed = dec.slice(8 + 32);
|
||||||
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
|
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
|
||||||
@ -1263,6 +1282,20 @@ export async function decryptContractForMerge(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptContractForDeposit() {
|
export async function decryptContractForDeposit(
|
||||||
throw Error("not implemented");
|
enc: OpaqueData,
|
||||||
|
pursePub: PursePublicKey,
|
||||||
|
contractPriv: ContractPrivateKey,
|
||||||
|
): Promise<DecryptForDepositResult> {
|
||||||
|
const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
|
||||||
|
const dec = await decryptWithDerivedKey(enc, key, depositSalt);
|
||||||
|
const contractTermsCompressed = dec.slice(8);
|
||||||
|
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 {
|
||||||
|
contractTerms: JSON.parse(contractTermsString),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1874,3 +1874,74 @@ export interface PeerContractTerms {
|
|||||||
summary: string;
|
summary: string;
|
||||||
purse_expiration: TalerProtocolTimestamp;
|
purse_expiration: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EncryptedContract {
|
||||||
|
// Encrypted contract.
|
||||||
|
econtract: string;
|
||||||
|
|
||||||
|
// Signature over the (encrypted) contract.
|
||||||
|
econtract_sig: string;
|
||||||
|
|
||||||
|
// Ephemeral public key for the DH operation to decrypt the encrypted contract.
|
||||||
|
contract_pub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for /reserves/{reserve_pub}/purse
|
||||||
|
* endpoint of the exchange.
|
||||||
|
*/
|
||||||
|
export interface ExchangeReservePurseRequest {
|
||||||
|
/**
|
||||||
|
* Minimum amount that must be credited to the reserve, that is
|
||||||
|
* the total value of the purse minus the deposit fees.
|
||||||
|
* If the deposit fees are lower, the contribution to the
|
||||||
|
* reserve can be higher!
|
||||||
|
*/
|
||||||
|
purse_value: AmountString;
|
||||||
|
|
||||||
|
// Minimum age required for all coins deposited into the purse.
|
||||||
|
min_age: number;
|
||||||
|
|
||||||
|
// Purse fee the reserve owner is willing to pay
|
||||||
|
// for the purse creation. Optional, if not present
|
||||||
|
// the purse is to be created from the purse quota
|
||||||
|
// of the reserve.
|
||||||
|
purse_fee: AmountString;
|
||||||
|
|
||||||
|
// Optional encrypted contract, in case the buyer is
|
||||||
|
// proposing the contract and thus establishing the
|
||||||
|
// purse with the payment.
|
||||||
|
econtract?: EncryptedContract;
|
||||||
|
|
||||||
|
// EdDSA public key used to approve merges of this purse.
|
||||||
|
merge_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// EdDSA signature of the account/reserve affirming the merge.
|
||||||
|
// Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
|
||||||
|
reserve_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// Purse public key.
|
||||||
|
purse_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// EdDSA signature of the purse over
|
||||||
|
// TALER_PurseRequestSignaturePS of
|
||||||
|
// purpose TALER_SIGNATURE_PURSE_REQUEST
|
||||||
|
// confirming that the
|
||||||
|
// above details hold for this purse.
|
||||||
|
purse_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// SHA-512 hash of the contact of the purse.
|
||||||
|
h_contract_terms: HashCodeString;
|
||||||
|
|
||||||
|
// Client-side timestamp of when the merge request was made.
|
||||||
|
merge_timestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
// Indicative time by which the purse should expire
|
||||||
|
// if it has not been paid.
|
||||||
|
purse_expiration: TalerProtocolTimestamp;
|
||||||
|
}
|
||||||
|
@ -45,6 +45,11 @@ export interface PayPushUriResult {
|
|||||||
contractPriv: string;
|
contractPriv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PayPullUriResult {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a taler[+http]://withdraw URI.
|
* Parse a taler[+http]://withdraw URI.
|
||||||
* Return undefined if not passed a valid URI.
|
* Return undefined if not passed a valid URI.
|
||||||
@ -84,10 +89,14 @@ export enum TalerUriType {
|
|||||||
TalerTip = "taler-tip",
|
TalerTip = "taler-tip",
|
||||||
TalerRefund = "taler-refund",
|
TalerRefund = "taler-refund",
|
||||||
TalerNotifyReserve = "taler-notify-reserve",
|
TalerNotifyReserve = "taler-notify-reserve",
|
||||||
TalerPayPush = "pay-push",
|
TalerPayPush = "taler-pay-push",
|
||||||
|
TalerPayPull = "taler-pay-pull",
|
||||||
Unknown = "unknown",
|
Unknown = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const talerActionPayPull = "pay-pull";
|
||||||
|
const talerActionPayPush = "pay-push";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify a taler:// URI.
|
* Classify a taler:// URI.
|
||||||
*/
|
*/
|
||||||
@ -117,12 +126,18 @@ export function classifyTalerUri(s: string): TalerUriType {
|
|||||||
if (sl.startsWith("taler+http://withdraw/")) {
|
if (sl.startsWith("taler+http://withdraw/")) {
|
||||||
return TalerUriType.TalerWithdraw;
|
return TalerUriType.TalerWithdraw;
|
||||||
}
|
}
|
||||||
if (sl.startsWith("taler://pay-push/")) {
|
if (sl.startsWith(`taler://${talerActionPayPush}/`)) {
|
||||||
return TalerUriType.TalerPayPush;
|
return TalerUriType.TalerPayPush;
|
||||||
}
|
}
|
||||||
if (sl.startsWith("taler+http://pay-push/")) {
|
if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {
|
||||||
return TalerUriType.TalerPayPush;
|
return TalerUriType.TalerPayPush;
|
||||||
}
|
}
|
||||||
|
if (sl.startsWith(`taler://${talerActionPayPull}/`)) {
|
||||||
|
return TalerUriType.TalerPayPull;
|
||||||
|
}
|
||||||
|
if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) {
|
||||||
|
return TalerUriType.TalerPayPull;
|
||||||
|
}
|
||||||
if (sl.startsWith("taler://notify-reserve/")) {
|
if (sl.startsWith("taler://notify-reserve/")) {
|
||||||
return TalerUriType.TalerNotifyReserve;
|
return TalerUriType.TalerNotifyReserve;
|
||||||
}
|
}
|
||||||
@ -189,7 +204,29 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
|
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
|
||||||
const pi = parseProtoInfo(s, "pay-push");
|
const pi = parseProtoInfo(s, talerActionPayPush);
|
||||||
|
if (!pi) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const c = pi?.rest.split("?");
|
||||||
|
const parts = c[0].split("/");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const host = parts[0].toLowerCase();
|
||||||
|
const contractPriv = parts[parts.length - 1];
|
||||||
|
const pathSegments = parts.slice(1, parts.length - 1);
|
||||||
|
const p = [host, ...pathSegments].join("/");
|
||||||
|
const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exchangeBaseUrl,
|
||||||
|
contractPriv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
|
||||||
|
const pi = parseProtoInfo(s, talerActionPayPull);
|
||||||
if (!pi) {
|
if (!pi) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -283,3 +320,24 @@ export function constructPayPushUri(args: {
|
|||||||
}
|
}
|
||||||
return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
|
return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function constructPayPullUri(args: {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}): string {
|
||||||
|
const url = new URL(args.exchangeBaseUrl);
|
||||||
|
let proto: string;
|
||||||
|
if (url.protocol === "https:") {
|
||||||
|
proto = "taler";
|
||||||
|
} else if (url.protocol === "http:") {
|
||||||
|
proto = "taler+http";
|
||||||
|
} else {
|
||||||
|
throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
|
||||||
|
}
|
||||||
|
if (!url.pathname.endsWith("/")) {
|
||||||
|
throw Error(
|
||||||
|
`exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`;
|
||||||
|
}
|
||||||
|
@ -627,7 +627,7 @@ export interface ExchangeAccount {
|
|||||||
master_sig: string;
|
master_sig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WireFeeMap = { [wireMethod: string]: WireFee[] }
|
export type WireFeeMap = { [wireMethod: string]: WireFee[] };
|
||||||
export interface WireInfo {
|
export interface WireInfo {
|
||||||
feesForType: WireFeeMap;
|
feesForType: WireFeeMap;
|
||||||
accounts: ExchangeAccount[];
|
accounts: ExchangeAccount[];
|
||||||
@ -639,7 +639,6 @@ const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
|
|||||||
.property("master_sig", codecForString())
|
.property("master_sig", codecForString())
|
||||||
.build("codecForExchangeAccount");
|
.build("codecForExchangeAccount");
|
||||||
|
|
||||||
|
|
||||||
const codecForWireFee = (): Codec<WireFee> =>
|
const codecForWireFee = (): Codec<WireFee> =>
|
||||||
buildCodecForObject<WireFee>()
|
buildCodecForObject<WireFee>()
|
||||||
.property("sig", codecForString())
|
.property("sig", codecForString())
|
||||||
@ -658,19 +657,18 @@ const codecForWireInfo = (): Codec<WireInfo> =>
|
|||||||
|
|
||||||
const codecForDenominationInfo = (): Codec<DenominationInfo> =>
|
const codecForDenominationInfo = (): Codec<DenominationInfo> =>
|
||||||
buildCodecForObject<DenominationInfo>()
|
buildCodecForObject<DenominationInfo>()
|
||||||
.property("denomPubHash", (codecForString()))
|
.property("denomPubHash", codecForString())
|
||||||
.property("value", (codecForAmountJson()))
|
.property("value", codecForAmountJson())
|
||||||
.property("feeWithdraw", (codecForAmountJson()))
|
.property("feeWithdraw", codecForAmountJson())
|
||||||
.property("feeDeposit", (codecForAmountJson()))
|
.property("feeDeposit", codecForAmountJson())
|
||||||
.property("feeRefresh", (codecForAmountJson()))
|
.property("feeRefresh", codecForAmountJson())
|
||||||
.property("feeRefund", (codecForAmountJson()))
|
.property("feeRefund", codecForAmountJson())
|
||||||
.property("stampStart", (codecForTimestamp))
|
.property("stampStart", codecForTimestamp)
|
||||||
.property("stampExpireWithdraw", (codecForTimestamp))
|
.property("stampExpireWithdraw", codecForTimestamp)
|
||||||
.property("stampExpireLegal", (codecForTimestamp))
|
.property("stampExpireLegal", codecForTimestamp)
|
||||||
.property("stampExpireDeposit", (codecForTimestamp))
|
.property("stampExpireDeposit", codecForTimestamp)
|
||||||
.build("codecForDenominationInfo");
|
.build("codecForDenominationInfo");
|
||||||
|
|
||||||
|
|
||||||
export interface DenominationInfo {
|
export interface DenominationInfo {
|
||||||
value: AmountJson;
|
value: AmountJson;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
@ -713,7 +711,6 @@ export interface DenominationInfo {
|
|||||||
* Data after which coins of this denomination can't be deposited anymore.
|
* Data after which coins of this denomination can't be deposited anymore.
|
||||||
*/
|
*/
|
||||||
stampExpireDeposit: TalerProtocolTimestamp;
|
stampExpireDeposit: TalerProtocolTimestamp;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeListItem {
|
export interface ExchangeListItem {
|
||||||
@ -726,7 +723,6 @@ export interface ExchangeListItem {
|
|||||||
denominations: DenominationInfo[];
|
denominations: DenominationInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
|
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
|
||||||
buildCodecForObject<AuditorDenomSig>()
|
buildCodecForObject<AuditorDenomSig>()
|
||||||
.property("denom_pub_h", codecForString())
|
.property("denom_pub_h", codecForString())
|
||||||
@ -740,7 +736,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
|
|||||||
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
|
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
|
||||||
.build("codecForExchangeAuditor");
|
.build("codecForExchangeAuditor");
|
||||||
|
|
||||||
|
|
||||||
const codecForExchangeTos = (): Codec<ExchangeTos> =>
|
const codecForExchangeTos = (): Codec<ExchangeTos> =>
|
||||||
buildCodecForObject<ExchangeTos>()
|
buildCodecForObject<ExchangeTos>()
|
||||||
.property("acceptedVersion", codecOptional(codecForString()))
|
.property("acceptedVersion", codecOptional(codecForString()))
|
||||||
@ -1452,18 +1447,34 @@ export interface CheckPeerPushPaymentRequest {
|
|||||||
talerUri: string;
|
talerUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CheckPeerPullPaymentRequest {
|
||||||
|
talerUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CheckPeerPushPaymentResponse {
|
export interface CheckPeerPushPaymentResponse {
|
||||||
contractTerms: any;
|
contractTerms: any;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
peerPushPaymentIncomingId: string;
|
peerPushPaymentIncomingId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CheckPeerPullPaymentResponse {
|
||||||
|
contractTerms: any;
|
||||||
|
amount: AmountString;
|
||||||
|
peerPullPaymentIncomingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForCheckPeerPushPaymentRequest =
|
export const codecForCheckPeerPushPaymentRequest =
|
||||||
(): Codec<CheckPeerPushPaymentRequest> =>
|
(): Codec<CheckPeerPushPaymentRequest> =>
|
||||||
buildCodecForObject<CheckPeerPushPaymentRequest>()
|
buildCodecForObject<CheckPeerPushPaymentRequest>()
|
||||||
.property("talerUri", codecForString())
|
.property("talerUri", codecForString())
|
||||||
.build("CheckPeerPushPaymentRequest");
|
.build("CheckPeerPushPaymentRequest");
|
||||||
|
|
||||||
|
export const codecForCheckPeerPullPaymentRequest =
|
||||||
|
(): Codec<CheckPeerPullPaymentRequest> =>
|
||||||
|
buildCodecForObject<CheckPeerPullPaymentRequest>()
|
||||||
|
.property("talerUri", codecForString())
|
||||||
|
.build("CheckPeerPullPaymentRequest");
|
||||||
|
|
||||||
export interface AcceptPeerPushPaymentRequest {
|
export interface AcceptPeerPushPaymentRequest {
|
||||||
/**
|
/**
|
||||||
* Transparent identifier of the incoming peer push payment.
|
* Transparent identifier of the incoming peer push payment.
|
||||||
@ -1476,3 +1487,41 @@ export const codecForAcceptPeerPushPaymentRequest =
|
|||||||
buildCodecForObject<AcceptPeerPushPaymentRequest>()
|
buildCodecForObject<AcceptPeerPushPaymentRequest>()
|
||||||
.property("peerPushPaymentIncomingId", codecForString())
|
.property("peerPushPaymentIncomingId", codecForString())
|
||||||
.build("AcceptPeerPushPaymentRequest");
|
.build("AcceptPeerPushPaymentRequest");
|
||||||
|
|
||||||
|
export interface AcceptPeerPullPaymentRequest {
|
||||||
|
/**
|
||||||
|
* Transparent identifier of the incoming peer pull payment.
|
||||||
|
*/
|
||||||
|
peerPullPaymentIncomingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForAcceptPeerPullPaymentRequest =
|
||||||
|
(): Codec<AcceptPeerPullPaymentRequest> =>
|
||||||
|
buildCodecForObject<AcceptPeerPullPaymentRequest>()
|
||||||
|
.property("peerPullPaymentIncomingId", codecForString())
|
||||||
|
.build("AcceptPeerPllPaymentRequest");
|
||||||
|
|
||||||
|
export interface InitiatePeerPullPaymentRequest {
|
||||||
|
/**
|
||||||
|
* FIXME: Make this optional?
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
amount: AmountString;
|
||||||
|
partialContractTerms: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForInitiatePeerPullPaymentRequest =
|
||||||
|
(): Codec<InitiatePeerPullPaymentRequest> =>
|
||||||
|
buildCodecForObject<InitiatePeerPullPaymentRequest>()
|
||||||
|
.property("partialContractTerms", codecForAny())
|
||||||
|
.property("amount", codecForAmountString())
|
||||||
|
.property("exchangeBaseUrl", codecForAmountString())
|
||||||
|
.build("InitiatePeerPullPaymentRequest");
|
||||||
|
|
||||||
|
export interface InitiatePeerPullPaymentResponse {
|
||||||
|
/**
|
||||||
|
* Taler URI for the other party to make the payment
|
||||||
|
* that was requested.
|
||||||
|
*/
|
||||||
|
talerUri: string;
|
||||||
|
}
|
||||||
|
@ -1284,7 +1284,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
// account fee
|
// account fee
|
||||||
`${this.exchangeConfig.currency}:0.01`,
|
`${this.exchangeConfig.currency}:0.01`,
|
||||||
// purse fee
|
// purse fee
|
||||||
`${this.exchangeConfig.currency}:0.01`,
|
`${this.exchangeConfig.currency}:0.00`,
|
||||||
// purse timeout
|
// purse timeout
|
||||||
"1h",
|
"1h",
|
||||||
// kyc timeout
|
// kyc timeout
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { GlobalTestState } from "../harness/harness.js";
|
||||||
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
} from "../harness/helpers.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
|
*/
|
||||||
|
export async function runPeerToPeerPullTest(t: GlobalTestState) {
|
||||||
|
// Set up test environment
|
||||||
|
|
||||||
|
const { wallet, bank, exchange, merchant } =
|
||||||
|
await createSimpleTestkudosEnvironment(t);
|
||||||
|
|
||||||
|
// Withdraw digital cash into the wallet.
|
||||||
|
|
||||||
|
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
|
||||||
|
const resp = await wallet.client.call(
|
||||||
|
WalletApiOperation.InitiatePeerPullPayment,
|
||||||
|
{
|
||||||
|
exchangeBaseUrl: exchange.baseUrl,
|
||||||
|
amount: "TESTKUDOS:5",
|
||||||
|
partialContractTerms: {
|
||||||
|
summary: "Hello World",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkResp = await wallet.client.call(
|
||||||
|
WalletApiOperation.CheckPeerPullPayment,
|
||||||
|
{
|
||||||
|
talerUri: resp.talerUri,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const acceptResp = await wallet.client.call(
|
||||||
|
WalletApiOperation.AcceptPeerPullPayment,
|
||||||
|
{
|
||||||
|
peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
runPeerToPeerPullTest.suites = ["wallet"];
|
@ -27,7 +27,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Run test for basic, bank-integrated withdrawal and payment.
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
*/
|
*/
|
||||||
export async function runPeerToPeerTest(t: GlobalTestState) {
|
export async function runPeerToPeerPushTest(t: GlobalTestState) {
|
||||||
// Set up test environment
|
// Set up test environment
|
||||||
|
|
||||||
const { wallet, bank, exchange, merchant } =
|
const { wallet, bank, exchange, merchant } =
|
||||||
@ -72,4 +72,4 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
|
|||||||
await wallet.runUntilDone();
|
await wallet.runUntilDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
runPeerToPeerTest.suites = ["wallet"];
|
runPeerToPeerPushTest.suites = ["wallet"];
|
@ -73,7 +73,8 @@ import { runPaymentDemoTest } from "./test-payment-on-demo";
|
|||||||
import { runPaymentTransientTest } from "./test-payment-transient";
|
import { runPaymentTransientTest } from "./test-payment-transient";
|
||||||
import { runPaymentZeroTest } from "./test-payment-zero.js";
|
import { runPaymentZeroTest } from "./test-payment-zero.js";
|
||||||
import { runPaywallFlowTest } from "./test-paywall-flow";
|
import { runPaywallFlowTest } from "./test-paywall-flow";
|
||||||
import { runPeerToPeerTest } from "./test-peer-to-peer.js";
|
import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js";
|
||||||
|
import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js";
|
||||||
import { runRefundTest } from "./test-refund";
|
import { runRefundTest } from "./test-refund";
|
||||||
import { runRefundAutoTest } from "./test-refund-auto";
|
import { runRefundAutoTest } from "./test-refund-auto";
|
||||||
import { runRefundGoneTest } from "./test-refund-gone";
|
import { runRefundGoneTest } from "./test-refund-gone";
|
||||||
@ -154,7 +155,8 @@ const allTests: TestMainFunction[] = [
|
|||||||
runPaymentZeroTest,
|
runPaymentZeroTest,
|
||||||
runPayPaidTest,
|
runPayPaidTest,
|
||||||
runPaywallFlowTest,
|
runPaywallFlowTest,
|
||||||
runPeerToPeerTest,
|
runPeerToPeerPushTest,
|
||||||
|
runPeerToPeerPullTest,
|
||||||
runRefundAutoTest,
|
runRefundAutoTest,
|
||||||
runRefundGoneTest,
|
runRefundGoneTest,
|
||||||
runRefundIncrementalTest,
|
runRefundIncrementalTest,
|
||||||
|
@ -33,11 +33,11 @@ import {
|
|||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
bufferForUint32,
|
bufferForUint32,
|
||||||
buildSigPS,
|
buildSigPS,
|
||||||
bytesToString,
|
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
createHashContext,
|
createHashContext,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
|
decryptContractForDeposit,
|
||||||
decryptContractForMerge,
|
decryptContractForMerge,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
@ -47,6 +47,7 @@ import {
|
|||||||
eddsaSign,
|
eddsaSign,
|
||||||
eddsaVerify,
|
eddsaVerify,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
|
encryptContractForDeposit,
|
||||||
encryptContractForMerge,
|
encryptContractForMerge,
|
||||||
ExchangeProtocolVersion,
|
ExchangeProtocolVersion,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
@ -85,17 +86,23 @@ import { DenominationRecord } from "../db.js";
|
|||||||
import {
|
import {
|
||||||
CreateRecoupRefreshReqRequest,
|
CreateRecoupRefreshReqRequest,
|
||||||
CreateRecoupReqRequest,
|
CreateRecoupReqRequest,
|
||||||
|
DecryptContractForDepositRequest,
|
||||||
|
DecryptContractForDepositResponse,
|
||||||
DecryptContractRequest,
|
DecryptContractRequest,
|
||||||
DecryptContractResponse,
|
DecryptContractResponse,
|
||||||
DerivedRefreshSession,
|
DerivedRefreshSession,
|
||||||
DerivedTipPlanchet,
|
DerivedTipPlanchet,
|
||||||
DeriveRefreshSessionRequest,
|
DeriveRefreshSessionRequest,
|
||||||
DeriveTipRequest,
|
DeriveTipRequest,
|
||||||
|
EncryptContractForDepositRequest,
|
||||||
|
EncryptContractForDepositResponse,
|
||||||
EncryptContractRequest,
|
EncryptContractRequest,
|
||||||
EncryptContractResponse,
|
EncryptContractResponse,
|
||||||
EncryptedContract,
|
EncryptedContract,
|
||||||
SignPurseMergeRequest,
|
SignPurseMergeRequest,
|
||||||
SignPurseMergeResponse,
|
SignPurseMergeResponse,
|
||||||
|
SignReservePurseCreateRequest,
|
||||||
|
SignReservePurseCreateResponse,
|
||||||
SignTrackTransactionRequest,
|
SignTrackTransactionRequest,
|
||||||
} from "./cryptoTypes.js";
|
} from "./cryptoTypes.js";
|
||||||
|
|
||||||
@ -205,7 +212,19 @@ export interface TalerCryptoInterface {
|
|||||||
req: DecryptContractRequest,
|
req: DecryptContractRequest,
|
||||||
): Promise<DecryptContractResponse>;
|
): Promise<DecryptContractResponse>;
|
||||||
|
|
||||||
|
encryptContractForDeposit(
|
||||||
|
req: EncryptContractForDepositRequest,
|
||||||
|
): Promise<EncryptContractForDepositResponse>;
|
||||||
|
|
||||||
|
decryptContractForDeposit(
|
||||||
|
req: DecryptContractForDepositRequest,
|
||||||
|
): Promise<DecryptContractForDepositResponse>;
|
||||||
|
|
||||||
signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
|
signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
|
||||||
|
|
||||||
|
signReservePurseCreate(
|
||||||
|
req: SignReservePurseCreateRequest,
|
||||||
|
): Promise<SignReservePurseCreateResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -362,6 +381,21 @@ export const nullCrypto: TalerCryptoInterface = {
|
|||||||
): Promise<SignPurseMergeResponse> {
|
): Promise<SignPurseMergeResponse> {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
},
|
},
|
||||||
|
encryptContractForDeposit: function (
|
||||||
|
req: EncryptContractForDepositRequest,
|
||||||
|
): Promise<EncryptContractForDepositResponse> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
decryptContractForDeposit: function (
|
||||||
|
req: DecryptContractForDepositRequest,
|
||||||
|
): Promise<DecryptContractForDepositResponse> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
signReservePurseCreate: function (
|
||||||
|
req: SignReservePurseCreateRequest,
|
||||||
|
): Promise<SignReservePurseCreateResponse> {
|
||||||
|
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
|
||||||
@ -1047,7 +1081,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
|
|
||||||
if (depositInfo.requiredMinimumAge != null) {
|
if (depositInfo.requiredMinimumAge != null) {
|
||||||
s.minimum_age_sig = minimumAgeSig;
|
s.minimum_age_sig = minimumAgeSig;
|
||||||
s.age_commitment = depositInfo.ageCommitmentProof?.commitment.publicKeys;
|
s.age_commitment =
|
||||||
|
depositInfo.ageCommitmentProof?.commitment.publicKeys;
|
||||||
} else if (depositInfo.ageCommitmentProof) {
|
} else if (depositInfo.ageCommitmentProof) {
|
||||||
(s as any).h_age_commitment = hAgeCommitment;
|
(s as any).h_age_commitment = hAgeCommitment;
|
||||||
}
|
}
|
||||||
@ -1389,6 +1424,43 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
mergePriv: encodeCrock(res.mergePriv),
|
mergePriv: encodeCrock(res.mergePriv),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async encryptContractForDeposit(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: EncryptContractForDepositRequest,
|
||||||
|
): Promise<EncryptContractForDepositResponse> {
|
||||||
|
const contractKeyPair = await this.createEddsaKeypair(tci, {});
|
||||||
|
const enc = await encryptContractForDeposit(
|
||||||
|
decodeCrock(req.pursePub),
|
||||||
|
decodeCrock(contractKeyPair.priv),
|
||||||
|
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 decryptContractForDeposit(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: DecryptContractForDepositRequest,
|
||||||
|
): Promise<DecryptContractForDepositResponse> {
|
||||||
|
const res = await decryptContractForDeposit(
|
||||||
|
decodeCrock(req.ciphertext),
|
||||||
|
decodeCrock(req.pursePub),
|
||||||
|
decodeCrock(req.contractPriv),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
contractTerms: res.contractTerms,
|
||||||
|
};
|
||||||
|
},
|
||||||
async signPurseMerge(
|
async signPurseMerge(
|
||||||
tci: TalerCryptoInterfaceR,
|
tci: TalerCryptoInterfaceR,
|
||||||
req: SignPurseMergeRequest,
|
req: SignPurseMergeRequest,
|
||||||
@ -1431,6 +1503,70 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
accountSig: reserveSigResp.sig,
|
accountSig: reserveSigResp.sig,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async signReservePurseCreate(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: SignReservePurseCreateRequest,
|
||||||
|
): Promise<SignReservePurseCreateResponse> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`payto URI: ${req.reservePayto}`);
|
||||||
|
logger.info(
|
||||||
|
`signing WALLET_PURSE_MERGE over ${encodeCrock(mergeSigBlob)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergePub = encodeCrock(eddsaGetPublic(decodeCrock(req.mergePriv)));
|
||||||
|
|
||||||
|
const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
|
||||||
|
.put(timestampRoundedToBuffer(req.purseExpiration))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
|
||||||
|
.put(decodeCrock(req.contractTermsHash))
|
||||||
|
.put(decodeCrock(mergePub))
|
||||||
|
// FIXME: add age!
|
||||||
|
.put(bufferForUint32(0))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const purseSigResp = await tci.eddsaSign(tci, {
|
||||||
|
msg: encodeCrock(purseSigBlob),
|
||||||
|
priv: req.pursePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mergeSig: mergeSigResp.sig,
|
||||||
|
accountSig: reserveSigResp.sig,
|
||||||
|
purseSig: purseSigResp.sig,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||||
|
@ -187,6 +187,19 @@ export interface EncryptContractResponse {
|
|||||||
contractPriv: string;
|
contractPriv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EncryptContractForDepositRequest {
|
||||||
|
contractTerms: any;
|
||||||
|
|
||||||
|
pursePub: string;
|
||||||
|
pursePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncryptContractForDepositResponse {
|
||||||
|
econtract: EncryptedContract;
|
||||||
|
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DecryptContractRequest {
|
export interface DecryptContractRequest {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
pursePub: string;
|
pursePub: string;
|
||||||
@ -198,6 +211,16 @@ export interface DecryptContractResponse {
|
|||||||
mergePriv: string;
|
mergePriv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DecryptContractForDepositRequest {
|
||||||
|
ciphertext: string;
|
||||||
|
pursePub: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecryptContractForDepositResponse {
|
||||||
|
contractTerms: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SignPurseMergeRequest {
|
export interface SignPurseMergeRequest {
|
||||||
mergeTimestamp: TalerProtocolTimestamp;
|
mergeTimestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
@ -230,3 +253,44 @@ export interface SignPurseMergeResponse {
|
|||||||
|
|
||||||
accountSig: string;
|
accountSig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SignReservePurseCreateRequest {
|
||||||
|
mergeTimestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
pursePub: string;
|
||||||
|
|
||||||
|
pursePriv: string;
|
||||||
|
|
||||||
|
reservePayto: string;
|
||||||
|
|
||||||
|
reservePriv: string;
|
||||||
|
|
||||||
|
mergePriv: string;
|
||||||
|
|
||||||
|
purseExpiration: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
purseAmount: AmountString;
|
||||||
|
purseFee: AmountString;
|
||||||
|
|
||||||
|
contractTermsHash: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags.
|
||||||
|
*/
|
||||||
|
flags: WalletAccountMergeFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response with signatures needed for creation of a purse
|
||||||
|
* from a reserve for a PULL payment.
|
||||||
|
*/
|
||||||
|
export interface SignReservePurseCreateResponse {
|
||||||
|
/**
|
||||||
|
* Signature made by the purse's merge private key.
|
||||||
|
*/
|
||||||
|
mergeSig: string;
|
||||||
|
|
||||||
|
accountSig: string;
|
||||||
|
|
||||||
|
purseSig: string;
|
||||||
|
}
|
||||||
|
@ -393,7 +393,6 @@ export interface ExchangeDetailsRecord {
|
|||||||
wireInfo: WireInfo;
|
wireInfo: WireInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ExchangeDetailsPointer {
|
export interface ExchangeDetailsPointer {
|
||||||
masterPublicKey: string;
|
masterPublicKey: string;
|
||||||
|
|
||||||
@ -922,7 +921,6 @@ export interface RefreshSessionRecord {
|
|||||||
norevealIndex?: number;
|
norevealIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export enum RefundState {
|
export enum RefundState {
|
||||||
Failed = "failed",
|
Failed = "failed",
|
||||||
Applied = "applied",
|
Applied = "applied",
|
||||||
@ -1625,6 +1623,36 @@ export interface PeerPushPaymentInitiationRecord {
|
|||||||
timestampCreated: TalerProtocolTimestamp;
|
timestampCreated: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PeerPullPaymentInitiationRecord {
|
||||||
|
/**
|
||||||
|
* What exchange are we using for the payment request?
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount requested.
|
||||||
|
*/
|
||||||
|
amount: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purse public key. Used as the primary key to look
|
||||||
|
* up this record.
|
||||||
|
*/
|
||||||
|
pursePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purse private key.
|
||||||
|
*/
|
||||||
|
pursePriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract terms for the other party.
|
||||||
|
*
|
||||||
|
* FIXME: Nail down type!
|
||||||
|
*/
|
||||||
|
contractTerms: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record for a push P2P payment that this wallet was offered.
|
* Record for a push P2P payment that this wallet was offered.
|
||||||
*
|
*
|
||||||
@ -1825,6 +1853,15 @@ export const WalletStoresV1 = {
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
peerPullPaymentInitiation: describeStore(
|
||||||
|
describeContents<PeerPullPaymentInitiationRecord>(
|
||||||
|
"peerPushPaymentInitiation",
|
||||||
|
{
|
||||||
|
keyPath: "pursePub",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MetaConfigRecord {
|
export interface MetaConfigRecord {
|
||||||
|
@ -37,7 +37,10 @@ import {
|
|||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
ExchangePurseMergeRequest,
|
ExchangePurseMergeRequest,
|
||||||
|
ExchangeReservePurseRequest,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
InitiatePeerPullPaymentRequest,
|
||||||
|
InitiatePeerPullPaymentResponse,
|
||||||
InitiatePeerPushPaymentRequest,
|
InitiatePeerPushPaymentRequest,
|
||||||
InitiatePeerPushPaymentResponse,
|
InitiatePeerPushPaymentResponse,
|
||||||
j2s,
|
j2s,
|
||||||
@ -370,6 +373,38 @@ export function talerPaytoFromExchangeReserve(
|
|||||||
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
|
return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getMergeReserveInfo(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
},
|
||||||
|
): Promise<MergeReserveInfo> {
|
||||||
|
// We have to eagerly create the key pair outside of the transaction,
|
||||||
|
// due to the async crypto API.
|
||||||
|
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const mergeReserveInfo: MergeReserveInfo = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
|
||||||
|
checkDbInvariant(!!ex);
|
||||||
|
if (ex.currentMergeReserveInfo) {
|
||||||
|
return ex.currentMergeReserveInfo;
|
||||||
|
}
|
||||||
|
await tx.exchanges.put(ex);
|
||||||
|
ex.currentMergeReserveInfo = {
|
||||||
|
reservePriv: newReservePair.priv,
|
||||||
|
reservePub: newReservePair.pub,
|
||||||
|
};
|
||||||
|
return ex.currentMergeReserveInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergeReserveInfo;
|
||||||
|
}
|
||||||
|
|
||||||
export async function acceptPeerPushPayment(
|
export async function acceptPeerPushPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: AcceptPeerPushPaymentRequest,
|
req: AcceptPeerPushPaymentRequest,
|
||||||
@ -388,27 +423,8 @@ export async function acceptPeerPushPayment(
|
|||||||
|
|
||||||
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
|
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
|
||||||
|
|
||||||
// We have to eagerly create the key pair outside of the transaction,
|
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
||||||
// due to the async crypto API.
|
exchangeBaseUrl: peerInc.exchangeBaseUrl,
|
||||||
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
|
||||||
|
|
||||||
const mergeReserveInfo: MergeReserveInfo = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
exchanges: x.exchanges,
|
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const ex = await tx.exchanges.get(peerInc.exchangeBaseUrl);
|
|
||||||
checkDbInvariant(!!ex);
|
|
||||||
if (ex.currentMergeReserveInfo) {
|
|
||||||
return ex.currentMergeReserveInfo;
|
|
||||||
}
|
|
||||||
await tx.exchanges.put(ex);
|
|
||||||
ex.currentMergeReserveInfo = {
|
|
||||||
reservePriv: newReservePair.priv,
|
|
||||||
reservePub: newReservePair.pub,
|
|
||||||
};
|
|
||||||
return ex.currentMergeReserveInfo;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergeTimestamp = TalerProtocolTimestamp.now();
|
const mergeTimestamp = TalerProtocolTimestamp.now();
|
||||||
@ -461,3 +477,115 @@ export async function acceptPeerPushPayment(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initiatePeerRequestForPay(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: InitiatePeerPullPaymentRequest,
|
||||||
|
): Promise<InitiatePeerPullPaymentResponse> {
|
||||||
|
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergeTimestamp = TalerProtocolTimestamp.now();
|
||||||
|
|
||||||
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
|
const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp(
|
||||||
|
AbsoluteTime.addDuration(
|
||||||
|
AbsoluteTime.now(),
|
||||||
|
Duration.fromSpec({ days: 2 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reservePayto = talerPaytoFromExchangeReserve(
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
mergeReserveInfo.reservePub,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractTerms = {
|
||||||
|
...req.partialContractTerms,
|
||||||
|
amount: req.amount,
|
||||||
|
purse_expiration: purseExpiration,
|
||||||
|
};
|
||||||
|
|
||||||
|
const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
|
||||||
|
contractTerms,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
|
||||||
|
|
||||||
|
const purseFee = Amounts.stringify(
|
||||||
|
Amounts.getZero(Amounts.parseOrThrow(req.amount).currency),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sigRes = await ws.cryptoApi.signReservePurseCreate({
|
||||||
|
contractTermsHash: hContractTerms,
|
||||||
|
flags: WalletAccountMergeFlags.CreateWithPurseFee,
|
||||||
|
mergePriv: mergePair.priv,
|
||||||
|
mergeTimestamp: mergeTimestamp,
|
||||||
|
purseAmount: req.amount,
|
||||||
|
purseExpiration: purseExpiration,
|
||||||
|
purseFee: purseFee,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
reservePayto,
|
||||||
|
reservePriv: mergeReserveInfo.reservePriv,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
peerPullPaymentInitiation: x.peerPullPaymentInitiation,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.peerPullPaymentInitiation.put({
|
||||||
|
amount: req.amount,
|
||||||
|
contractTerms,
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const reservePurseReqBody: ExchangeReservePurseRequest = {
|
||||||
|
merge_sig: sigRes.mergeSig,
|
||||||
|
merge_timestamp: mergeTimestamp,
|
||||||
|
h_contract_terms: hContractTerms,
|
||||||
|
merge_pub: mergePair.pub,
|
||||||
|
min_age: 0,
|
||||||
|
purse_expiration: purseExpiration,
|
||||||
|
purse_fee: purseFee,
|
||||||
|
purse_pub: pursePair.pub,
|
||||||
|
purse_sig: sigRes.purseSig,
|
||||||
|
purse_value: req.amount,
|
||||||
|
reserve_sig: sigRes.accountSig,
|
||||||
|
econtract: econtractResp.econtract,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
|
||||||
|
|
||||||
|
const reservePurseMergeUrl = new URL(
|
||||||
|
`reserves/${mergeReserveInfo.reservePub}/purse`,
|
||||||
|
req.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpResp = await ws.http.postJson(
|
||||||
|
reservePurseMergeUrl.href,
|
||||||
|
reservePurseReqBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||||
|
|
||||||
|
logger.info(`reserve merge response: ${j2s(resp)}`);
|
||||||
|
|
||||||
|
// FIXME: Now create a withdrawal operation!
|
||||||
|
|
||||||
|
return {
|
||||||
|
talerUri: constructPayPushUri({
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
contractPriv: econtractResp.contractPriv,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
AcceptExchangeTosRequest,
|
AcceptExchangeTosRequest,
|
||||||
AcceptManualWithdrawalRequest,
|
AcceptManualWithdrawalRequest,
|
||||||
AcceptManualWithdrawalResult,
|
AcceptManualWithdrawalResult,
|
||||||
|
AcceptPeerPullPaymentRequest,
|
||||||
AcceptPeerPushPaymentRequest,
|
AcceptPeerPushPaymentRequest,
|
||||||
AcceptTipRequest,
|
AcceptTipRequest,
|
||||||
AcceptWithdrawalResponse,
|
AcceptWithdrawalResponse,
|
||||||
@ -35,6 +36,8 @@ import {
|
|||||||
ApplyRefundResponse,
|
ApplyRefundResponse,
|
||||||
BackupRecovery,
|
BackupRecovery,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
|
CheckPeerPullPaymentRequest,
|
||||||
|
CheckPeerPullPaymentResponse,
|
||||||
CheckPeerPushPaymentRequest,
|
CheckPeerPushPaymentRequest,
|
||||||
CheckPeerPushPaymentResponse,
|
CheckPeerPushPaymentResponse,
|
||||||
CoinDumpJson,
|
CoinDumpJson,
|
||||||
@ -49,6 +52,8 @@ import {
|
|||||||
GetExchangeTosResult,
|
GetExchangeTosResult,
|
||||||
GetWithdrawalDetailsForAmountRequest,
|
GetWithdrawalDetailsForAmountRequest,
|
||||||
GetWithdrawalDetailsForUriRequest,
|
GetWithdrawalDetailsForUriRequest,
|
||||||
|
InitiatePeerPullPaymentRequest,
|
||||||
|
InitiatePeerPullPaymentResponse,
|
||||||
InitiatePeerPushPaymentRequest,
|
InitiatePeerPushPaymentRequest,
|
||||||
InitiatePeerPushPaymentResponse,
|
InitiatePeerPushPaymentResponse,
|
||||||
IntegrationTestArgs,
|
IntegrationTestArgs,
|
||||||
@ -126,6 +131,9 @@ export enum WalletApiOperation {
|
|||||||
InitiatePeerPushPayment = "initiatePeerPushPayment",
|
InitiatePeerPushPayment = "initiatePeerPushPayment",
|
||||||
CheckPeerPushPayment = "checkPeerPushPayment",
|
CheckPeerPushPayment = "checkPeerPushPayment",
|
||||||
AcceptPeerPushPayment = "acceptPeerPushPayment",
|
AcceptPeerPushPayment = "acceptPeerPushPayment",
|
||||||
|
InitiatePeerPullPayment = "initiatePeerPullPayment",
|
||||||
|
CheckPeerPullPayment = "checkPeerPullPayment",
|
||||||
|
AcceptPeerPullPayment = "acceptPeerPullPayment",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WalletOperations = {
|
export type WalletOperations = {
|
||||||
@ -297,6 +305,18 @@ export type WalletOperations = {
|
|||||||
request: AcceptPeerPushPaymentRequest;
|
request: AcceptPeerPushPaymentRequest;
|
||||||
response: {};
|
response: {};
|
||||||
};
|
};
|
||||||
|
[WalletApiOperation.InitiatePeerPullPayment]: {
|
||||||
|
request: InitiatePeerPullPaymentRequest;
|
||||||
|
response: InitiatePeerPullPaymentResponse;
|
||||||
|
};
|
||||||
|
[WalletApiOperation.CheckPeerPullPayment]: {
|
||||||
|
request: CheckPeerPullPaymentRequest;
|
||||||
|
response: CheckPeerPullPaymentResponse;
|
||||||
|
};
|
||||||
|
[WalletApiOperation.AcceptPeerPullPayment]: {
|
||||||
|
request: AcceptPeerPullPaymentRequest;
|
||||||
|
response: {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestType<
|
export type RequestType<
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
codecForAcceptBankIntegratedWithdrawalRequest,
|
codecForAcceptBankIntegratedWithdrawalRequest,
|
||||||
codecForAcceptExchangeTosRequest,
|
codecForAcceptExchangeTosRequest,
|
||||||
codecForAcceptManualWithdrawalRequet,
|
codecForAcceptManualWithdrawalRequet,
|
||||||
|
codecForAcceptPeerPullPaymentRequest,
|
||||||
codecForAcceptPeerPushPaymentRequest,
|
codecForAcceptPeerPushPaymentRequest,
|
||||||
codecForAcceptTipRequest,
|
codecForAcceptTipRequest,
|
||||||
codecForAddExchangeRequest,
|
codecForAddExchangeRequest,
|
||||||
@ -50,6 +51,7 @@ import {
|
|||||||
codecForGetWithdrawalDetailsForAmountRequest,
|
codecForGetWithdrawalDetailsForAmountRequest,
|
||||||
codecForGetWithdrawalDetailsForUri,
|
codecForGetWithdrawalDetailsForUri,
|
||||||
codecForImportDbRequest,
|
codecForImportDbRequest,
|
||||||
|
codecForInitiatePeerPullPaymentRequest,
|
||||||
codecForInitiatePeerPushPaymentRequest,
|
codecForInitiatePeerPushPaymentRequest,
|
||||||
codecForIntegrationTestArgs,
|
codecForIntegrationTestArgs,
|
||||||
codecForListKnownBankAccounts,
|
codecForListKnownBankAccounts,
|
||||||
@ -150,6 +152,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
acceptPeerPushPayment,
|
acceptPeerPushPayment,
|
||||||
checkPeerPushPayment,
|
checkPeerPushPayment,
|
||||||
|
initiatePeerRequestForPay,
|
||||||
initiatePeerToPeerPush,
|
initiatePeerToPeerPush,
|
||||||
} from "./operations/peer-to-peer.js";
|
} from "./operations/peer-to-peer.js";
|
||||||
import { getPendingOperations } from "./operations/pending.js";
|
import { getPendingOperations } from "./operations/pending.js";
|
||||||
@ -455,11 +458,20 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
|
|||||||
for (const c of builtinAuditors) {
|
for (const c of builtinAuditors) {
|
||||||
await tx.auditorTrustStore.put(c);
|
await tx.auditorTrustStore.put(c);
|
||||||
}
|
}
|
||||||
for (const url of builtinExchanges) {
|
|
||||||
await updateExchangeFromUrl(ws, url, { forceNow: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// FIXME: make sure exchanges are added transactionally to
|
||||||
|
// DB in first-time default application
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const url of builtinExchanges) {
|
||||||
|
try {
|
||||||
|
await updateExchangeFromUrl(ws, url, { forceNow: true });
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`could not update builtin exchange ${url} during wallet initialization`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExchangeTos(
|
async function getExchangeTos(
|
||||||
@ -568,8 +580,9 @@ async function getExchanges(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const denominations = await tx.denominations.indexes
|
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.byExchangeBaseUrl.iter(r.baseUrl).toArray();
|
.iter(r.baseUrl)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
if (!denominations) {
|
if (!denominations) {
|
||||||
continue;
|
continue;
|
||||||
@ -1030,6 +1043,10 @@ async function dispatchRequestInternal(
|
|||||||
await acceptPeerPushPayment(ws, req);
|
await acceptPeerPushPayment(ws, req);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
case "initiatePeerPullPayment": {
|
||||||
|
const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
|
||||||
|
return await initiatePeerRequestForPay(ws, req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
||||||
|
Loading…
Reference in New Issue
Block a user