wallet-core: P2P push payments (still incomplete)
This commit is contained in:
parent
05cdbfb534
commit
b214934b75
@ -773,6 +773,8 @@ export enum TalerSignaturePurpose {
|
|||||||
WALLET_COIN_LINK = 1204,
|
WALLET_COIN_LINK = 1204,
|
||||||
WALLET_COIN_RECOUP_REFRESH = 1206,
|
WALLET_COIN_RECOUP_REFRESH = 1206,
|
||||||
WALLET_AGE_ATTESTATION = 1207,
|
WALLET_AGE_ATTESTATION = 1207,
|
||||||
|
WALLET_PURSE_CREATE = 1210,
|
||||||
|
WALLET_PURSE_DEPOSIT = 1211,
|
||||||
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,
|
||||||
|
@ -565,8 +565,8 @@ export interface MerchantAbortPayRefundDetails {
|
|||||||
refund_amount: string;
|
refund_amount: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for the refund.
|
* Fee for the refund.
|
||||||
*/
|
*/
|
||||||
refund_fee: string;
|
refund_fee: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1794,3 +1794,41 @@ export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
|
|||||||
.property("exchange_timestamp", codecForTimestamp)
|
.property("exchange_timestamp", codecForTimestamp)
|
||||||
.property("transaction_base_url", codecOptional(codecForString()))
|
.property("transaction_base_url", codecOptional(codecForString()))
|
||||||
.build("DepositSuccess");
|
.build("DepositSuccess");
|
||||||
|
|
||||||
|
export interface PurseDeposit {
|
||||||
|
/**
|
||||||
|
* Amount to be deposited, can be a fraction of the
|
||||||
|
* coin's total value.
|
||||||
|
*/
|
||||||
|
amount: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of denomination RSA key with which the coin is signed.
|
||||||
|
*/
|
||||||
|
denom_pub_hash: HashCodeString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange's unblinded RSA signature of the coin.
|
||||||
|
*/
|
||||||
|
ub_sig: UnblindedSignature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Age commitment hash for the coin, if the denomination is age-restricted.
|
||||||
|
*/
|
||||||
|
h_age_commitment?: HashCodeString;
|
||||||
|
|
||||||
|
// FIXME-Oec: proof of age is missing.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature over TALER_PurseDepositSignaturePS
|
||||||
|
* of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
|
||||||
|
* made by the customer with the
|
||||||
|
* coin's private key.
|
||||||
|
*/
|
||||||
|
coin_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key of the coin being deposited into the purse.
|
||||||
|
*/
|
||||||
|
coin_pub: EddsaPublicKeyString;
|
||||||
|
}
|
||||||
|
@ -32,10 +32,7 @@ import {
|
|||||||
codecForAmountJson,
|
codecForAmountJson,
|
||||||
codecForAmountString,
|
codecForAmountString,
|
||||||
} from "./amounts.js";
|
} from "./amounts.js";
|
||||||
import {
|
import { codecForTimestamp, TalerProtocolTimestamp } from "./time.js";
|
||||||
codecForTimestamp,
|
|
||||||
TalerProtocolTimestamp,
|
|
||||||
} from "./time.js";
|
|
||||||
import {
|
import {
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
codecForString,
|
codecForString,
|
||||||
@ -1230,15 +1227,14 @@ export interface ForcedCoinSel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TestPayResult {
|
export interface TestPayResult {
|
||||||
payCoinSelection: PayCoinSelection,
|
payCoinSelection: PayCoinSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of selecting coins, contains the exchange, and selected
|
* Result of selecting coins, contains the exchange, and selected
|
||||||
* coins with their denomination.
|
* coins with their denomination.
|
||||||
*/
|
*/
|
||||||
export interface PayCoinSelection {
|
export interface PayCoinSelection {
|
||||||
/**
|
/**
|
||||||
* Amount requested by the merchant.
|
* Amount requested by the merchant.
|
||||||
*/
|
*/
|
||||||
@ -1263,4 +1259,19 @@ export interface TestPayResult {
|
|||||||
* How much of the deposit fees is the customer paying?
|
* How much of the deposit fees is the customer paying?
|
||||||
*/
|
*/
|
||||||
customerDepositFees: AmountJson;
|
customerDepositFees: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InitiatePeerPushPaymentRequest {
|
||||||
|
amount: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitiatePeerPushPaymentResponse {
|
||||||
|
pursePub: string;
|
||||||
|
mergePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForInitiatePeerPushPaymentRequest =
|
||||||
|
(): Codec<InitiatePeerPushPaymentRequest> =>
|
||||||
|
buildCodecForObject<InitiatePeerPushPaymentRequest>()
|
||||||
|
.property("amount", codecForAmountString())
|
||||||
|
.build("InitiatePeerPushPaymentRequest");
|
||||||
|
@ -1296,6 +1296,36 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runCommand(
|
||||||
|
this.globalState,
|
||||||
|
"exchange-offline",
|
||||||
|
"taler-exchange-offline",
|
||||||
|
[
|
||||||
|
"-c",
|
||||||
|
this.configFilename,
|
||||||
|
"global-fee",
|
||||||
|
// year
|
||||||
|
"now",
|
||||||
|
// history fee
|
||||||
|
`${this.exchangeConfig.currency}:0.01`,
|
||||||
|
// kyc fee
|
||||||
|
`${this.exchangeConfig.currency}:0.01`,
|
||||||
|
// account fee
|
||||||
|
`${this.exchangeConfig.currency}:0.01`,
|
||||||
|
// purse fee
|
||||||
|
`${this.exchangeConfig.currency}:0.01`,
|
||||||
|
// purse timeout
|
||||||
|
"1h",
|
||||||
|
// kyc timeout
|
||||||
|
"1h",
|
||||||
|
// history expiration
|
||||||
|
"1year",
|
||||||
|
// free purses per account
|
||||||
|
"5",
|
||||||
|
"upload",
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeDenomination(denomPubHash: string) {
|
async revokeDenomination(denomPubHash: string) {
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
makeTestPayment,
|
||||||
|
} from "../harness/helpers.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
|
*/
|
||||||
|
export async function runPeerToPeerTest(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.InitiatePeerPushPayment,
|
||||||
|
{
|
||||||
|
amount: "TESTKUDOS:5",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
runPeerToPeerTest.suites = ["wallet"];
|
@ -73,6 +73,7 @@ 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 { 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";
|
||||||
@ -153,6 +154,7 @@ const allTests: TestMainFunction[] = [
|
|||||||
runPaymentZeroTest,
|
runPaymentZeroTest,
|
||||||
runPayPaidTest,
|
runPayPaidTest,
|
||||||
runPaywallFlowTest,
|
runPaywallFlowTest,
|
||||||
|
runPeerToPeerTest,
|
||||||
runRefundAutoTest,
|
runRefundAutoTest,
|
||||||
runRefundGoneTest,
|
runRefundGoneTest,
|
||||||
runRefundIncrementalTest,
|
runRefundIncrementalTest,
|
||||||
|
@ -24,33 +24,44 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// FIXME: Crypto should not use DB Types!
|
|
||||||
import {
|
import {
|
||||||
|
AgeCommitmentProof,
|
||||||
|
AgeRestriction,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
AmountString,
|
||||||
|
BlindedDenominationSignature,
|
||||||
|
bufferForUint32,
|
||||||
buildSigPS,
|
buildSigPS,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
createEddsaKeyPair,
|
|
||||||
createHashContext,
|
createHashContext,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
|
ecdheGetPublic,
|
||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
|
EddsaPublicKeyString,
|
||||||
eddsaSign,
|
eddsaSign,
|
||||||
eddsaVerify,
|
eddsaVerify,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
ExchangeProtocolVersion,
|
ExchangeProtocolVersion,
|
||||||
|
getRandomBytes,
|
||||||
hash,
|
hash,
|
||||||
|
HashCodeString,
|
||||||
hashCoinEv,
|
hashCoinEv,
|
||||||
hashCoinEvInner,
|
hashCoinEvInner,
|
||||||
|
hashCoinPub,
|
||||||
hashDenomPub,
|
hashDenomPub,
|
||||||
hashTruncate32,
|
hashTruncate32,
|
||||||
|
kdf,
|
||||||
|
kdfKw,
|
||||||
keyExchangeEcdheEddsa,
|
keyExchangeEcdheEddsa,
|
||||||
Logger,
|
Logger,
|
||||||
MakeSyncSignatureRequest,
|
MakeSyncSignatureRequest,
|
||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
WithdrawalPlanchet,
|
PlanchetUnblindInfo,
|
||||||
|
PurseDeposit,
|
||||||
RecoupRefreshRequest,
|
RecoupRefreshRequest,
|
||||||
RecoupRequest,
|
RecoupRequest,
|
||||||
RefreshPlanchetInfo,
|
RefreshPlanchetInfo,
|
||||||
@ -59,23 +70,14 @@ import {
|
|||||||
rsaVerify,
|
rsaVerify,
|
||||||
setupTipPlanchet,
|
setupTipPlanchet,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
TalerSignaturePurpose,
|
|
||||||
BlindedDenominationSignature,
|
|
||||||
UnblindedSignature,
|
|
||||||
PlanchetUnblindInfo,
|
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
kdfKw,
|
TalerSignaturePurpose,
|
||||||
bufferForUint32,
|
UnblindedSignature,
|
||||||
kdf,
|
WithdrawalPlanchet,
|
||||||
ecdheGetPublic,
|
|
||||||
getRandomBytes,
|
|
||||||
AgeCommitmentProof,
|
|
||||||
AgeRestriction,
|
|
||||||
hashCoinPub,
|
|
||||||
HashCodeString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import bigint from "big-integer";
|
import bigint from "big-integer";
|
||||||
import { DenominationRecord, TipCoinSource, WireFee } from "../db.js";
|
// FIXME: Crypto should not use DB Types!
|
||||||
|
import { DenominationRecord, WireFee } from "../db.js";
|
||||||
import {
|
import {
|
||||||
CreateRecoupRefreshReqRequest,
|
CreateRecoupRefreshReqRequest,
|
||||||
CreateRecoupReqRequest,
|
CreateRecoupReqRequest,
|
||||||
@ -177,6 +179,12 @@ export interface TalerCryptoInterface {
|
|||||||
setupRefreshTransferPub(
|
setupRefreshTransferPub(
|
||||||
req: SetupRefreshTransferPubRequest,
|
req: SetupRefreshTransferPubRequest,
|
||||||
): Promise<TransferPubResponse>;
|
): Promise<TransferPubResponse>;
|
||||||
|
|
||||||
|
signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>;
|
||||||
|
|
||||||
|
signPurseDeposits(
|
||||||
|
req: SignPurseDepositsRequest,
|
||||||
|
): Promise<SignPurseDepositsResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -308,6 +316,16 @@ export const nullCrypto: TalerCryptoInterface = {
|
|||||||
): Promise<TransferPubResponse> {
|
): Promise<TransferPubResponse> {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
},
|
},
|
||||||
|
signPurseCreation: function (
|
||||||
|
req: SignPurseCreationRequest,
|
||||||
|
): Promise<EddsaSigningResult> {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
signPurseDeposits: function (
|
||||||
|
req: SignPurseDepositsRequest,
|
||||||
|
): Promise<SignPurseDepositsResponse> {
|
||||||
|
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
|
||||||
@ -336,6 +354,31 @@ export interface SetupWithdrawalPlanchetRequest {
|
|||||||
coinNumber: number;
|
coinNumber: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SignPurseCreationRequest {
|
||||||
|
pursePriv: string;
|
||||||
|
purseExpiration: TalerProtocolTimestamp;
|
||||||
|
purseAmount: AmountString;
|
||||||
|
hContractTerms: HashCodeString;
|
||||||
|
mergePub: EddsaPublicKeyString;
|
||||||
|
minAge: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignPurseDepositsRequest {
|
||||||
|
pursePub: string;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
coins: {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
contribution: AmountString;
|
||||||
|
denomPubHash: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignPurseDepositsResponse {
|
||||||
|
deposits: PurseDeposit[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RsaVerificationRequest {
|
export interface RsaVerificationRequest {
|
||||||
hm: string;
|
hm: string;
|
||||||
sig: string;
|
sig: string;
|
||||||
@ -1212,6 +1255,51 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub,
|
transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async signPurseCreation(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: SignPurseCreationRequest,
|
||||||
|
): Promise<EddsaSigningResult> {
|
||||||
|
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
|
||||||
|
.put(timestampRoundedToBuffer(req.purseExpiration))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
|
||||||
|
.put(decodeCrock(req.hContractTerms))
|
||||||
|
.put(decodeCrock(req.mergePub))
|
||||||
|
.put(bufferForUint32(req.minAge))
|
||||||
|
.build();
|
||||||
|
return await tci.eddsaSign(tci, {
|
||||||
|
msg: encodeCrock(sigBlob),
|
||||||
|
priv: req.pursePriv,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async signPurseDeposits(
|
||||||
|
tci: TalerCryptoInterfaceR,
|
||||||
|
req: SignPurseDepositsRequest,
|
||||||
|
): Promise<SignPurseDepositsResponse> {
|
||||||
|
const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0"));
|
||||||
|
const deposits: PurseDeposit[] = [];
|
||||||
|
for (const c of req.coins) {
|
||||||
|
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
|
||||||
|
.put(decodeCrock(req.pursePub))
|
||||||
|
.put(hExchangeBaseUrl)
|
||||||
|
.build();
|
||||||
|
const sigResp = await tci.eddsaSign(tci, {
|
||||||
|
msg: encodeCrock(sigBlob),
|
||||||
|
priv: c.coinPriv,
|
||||||
|
});
|
||||||
|
deposits.push({
|
||||||
|
amount: c.contribution,
|
||||||
|
coin_pub: c.coinPub,
|
||||||
|
coin_sig: sigResp.sig,
|
||||||
|
denom_pub_hash: c.denomPubHash,
|
||||||
|
ub_sig: c.denomSig,
|
||||||
|
h_age_commitment: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
deposits,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||||
|
@ -148,4 +148,4 @@ export interface CreateRecoupRefreshReqRequest {
|
|||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
}
|
}
|
@ -1309,9 +1309,9 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
|
|||||||
*/
|
*/
|
||||||
export type ConfigRecord =
|
export type ConfigRecord =
|
||||||
| {
|
| {
|
||||||
key: typeof WALLET_BACKUP_STATE_KEY;
|
key: typeof WALLET_BACKUP_STATE_KEY;
|
||||||
value: WalletBackupConfState;
|
value: WalletBackupConfState;
|
||||||
}
|
}
|
||||||
| { key: "currencyDefaultsApplied"; value: boolean };
|
| { key: "currencyDefaultsApplied"; value: boolean };
|
||||||
|
|
||||||
export interface WalletBackupConfState {
|
export interface WalletBackupConfState {
|
||||||
@ -1497,17 +1497,17 @@ export enum BackupProviderStateTag {
|
|||||||
|
|
||||||
export type BackupProviderState =
|
export type BackupProviderState =
|
||||||
| {
|
| {
|
||||||
tag: BackupProviderStateTag.Provisional;
|
tag: BackupProviderStateTag.Provisional;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: BackupProviderStateTag.Ready;
|
tag: BackupProviderStateTag.Ready;
|
||||||
nextBackupTimestamp: TalerProtocolTimestamp;
|
nextBackupTimestamp: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: BackupProviderStateTag.Retrying;
|
tag: BackupProviderStateTag.Retrying;
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
lastError?: TalerErrorDetail;
|
lastError?: TalerErrorDetail;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BackupProviderTerms {
|
export interface BackupProviderTerms {
|
||||||
supportedProtocolVersion: string;
|
supportedProtocolVersion: string;
|
||||||
@ -1671,6 +1671,52 @@ export interface BalancePerCurrencyRecord {
|
|||||||
pendingOutgoing: AmountString;
|
pendingOutgoing: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record for a push P2P payment that this wallet initiated.
|
||||||
|
*/
|
||||||
|
export interface PeerPushPaymentInitiationRecord {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What exchange are funds coming from?
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
amount: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purse public key. Used as the primary key to look
|
||||||
|
* up this record.
|
||||||
|
*/
|
||||||
|
pursePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purse private key.
|
||||||
|
*/
|
||||||
|
pursePriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key of the merge capability of the purse.
|
||||||
|
*/
|
||||||
|
mergePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private key of the merge capability of the purse.
|
||||||
|
*/
|
||||||
|
mergePriv: string;
|
||||||
|
|
||||||
|
purseExpiration: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Did we successfully create the purse with the exchange?
|
||||||
|
*/
|
||||||
|
purseCreated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record for a push P2P payment that this wallet accepted.
|
||||||
|
*/
|
||||||
|
export interface PeerPushPaymentAcceptanceRecord {}
|
||||||
|
|
||||||
export const WalletStoresV1 = {
|
export const WalletStoresV1 = {
|
||||||
coins: describeStore(
|
coins: describeStore(
|
||||||
describeContents<CoinRecord>("coins", {
|
describeContents<CoinRecord>("coins", {
|
||||||
|
222
packages/taler-wallet-core/src/operations/peer-to-peer.ts
Normal file
222
packages/taler-wallet-core/src/operations/peer-to-peer.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2019 GNUnet e.V.
|
||||||
|
|
||||||
|
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 {
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
Logger,
|
||||||
|
InitiatePeerPushPaymentResponse,
|
||||||
|
InitiatePeerPushPaymentRequest,
|
||||||
|
strcmp,
|
||||||
|
CoinPublicKeyString,
|
||||||
|
j2s,
|
||||||
|
getRandomBytes,
|
||||||
|
Duration,
|
||||||
|
durationAdd,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
AbsoluteTime,
|
||||||
|
encodeCrock,
|
||||||
|
AmountString,
|
||||||
|
UnblindedSignature,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { CoinStatus } from "../db.js";
|
||||||
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
|
||||||
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
|
export interface PeerCoinSelection {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info of Coins that were selected.
|
||||||
|
*/
|
||||||
|
coins: {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
contribution: AmountString;
|
||||||
|
denomPubHash: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much of the deposit fees is the customer paying?
|
||||||
|
*/
|
||||||
|
depositFees: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoinInfo {
|
||||||
|
/**
|
||||||
|
* Public key of the coin.
|
||||||
|
*/
|
||||||
|
coinPub: string;
|
||||||
|
|
||||||
|
coinPriv: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit fee for the coin.
|
||||||
|
*/
|
||||||
|
feeDeposit: AmountJson;
|
||||||
|
|
||||||
|
value: AmountJson;
|
||||||
|
|
||||||
|
denomPubHash: string;
|
||||||
|
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initiatePeerToPeerPush(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: InitiatePeerPushPaymentRequest,
|
||||||
|
): Promise<InitiatePeerPushPaymentResponse> {
|
||||||
|
const instructedAmount = Amounts.parseOrThrow(req.amount);
|
||||||
|
const coinSelRes: PeerCoinSelection | undefined = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
coins: x.coins,
|
||||||
|
denominations: x.denominations,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const exchanges = await tx.exchanges.iter().toArray();
|
||||||
|
for (const exch of exchanges) {
|
||||||
|
if (exch.detailsPointer?.currency !== instructedAmount.currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const coins = (
|
||||||
|
await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
|
||||||
|
).filter((x) => x.status === CoinStatus.Fresh);
|
||||||
|
const coinInfos: CoinInfo[] = [];
|
||||||
|
for (const coin of coins) {
|
||||||
|
const denom = await ws.getDenomInfo(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
);
|
||||||
|
if (!denom) {
|
||||||
|
throw Error("denom not found");
|
||||||
|
}
|
||||||
|
coinInfos.push({
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
feeDeposit: denom.feeDeposit,
|
||||||
|
value: denom.value,
|
||||||
|
denomPubHash: denom.denomPubHash,
|
||||||
|
coinPriv: coin.coinPriv,
|
||||||
|
denomSig: coin.denomSig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (coinInfos.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
coinInfos.sort(
|
||||||
|
(o1, o2) =>
|
||||||
|
-Amounts.cmp(o1.value, o2.value) ||
|
||||||
|
strcmp(o1.denomPubHash, o2.denomPubHash),
|
||||||
|
);
|
||||||
|
let amountAcc = Amounts.getZero(instructedAmount.currency);
|
||||||
|
let depositFeesAcc = Amounts.getZero(instructedAmount.currency);
|
||||||
|
const resCoins: {
|
||||||
|
coinPub: string;
|
||||||
|
coinPriv: string;
|
||||||
|
contribution: AmountString;
|
||||||
|
denomPubHash: string;
|
||||||
|
denomSig: UnblindedSignature;
|
||||||
|
}[] = [];
|
||||||
|
for (const coin of coinInfos) {
|
||||||
|
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||||
|
const res: PeerCoinSelection = {
|
||||||
|
exchangeBaseUrl: exch.baseUrl,
|
||||||
|
coins: resCoins,
|
||||||
|
depositFees: depositFeesAcc,
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const gap = Amounts.add(
|
||||||
|
coin.feeDeposit,
|
||||||
|
Amounts.sub(instructedAmount, amountAcc).amount,
|
||||||
|
).amount;
|
||||||
|
const contrib = Amounts.min(gap, coin.value);
|
||||||
|
amountAcc = Amounts.add(
|
||||||
|
amountAcc,
|
||||||
|
Amounts.sub(contrib, coin.feeDeposit).amount,
|
||||||
|
).amount;
|
||||||
|
depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
|
||||||
|
resCoins.push({
|
||||||
|
coinPriv: coin.coinPriv,
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
contribution: Amounts.stringify(contrib),
|
||||||
|
denomPubHash: coin.denomPubHash,
|
||||||
|
denomSig: coin.denomSig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
logger.info(`selected p2p coins: ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
|
if (!coinSelRes) {
|
||||||
|
throw Error("insufficient balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
const hContractTerms = encodeCrock(getRandomBytes(64));
|
||||||
|
const purseExpiration = AbsoluteTime.toTimestamp(
|
||||||
|
AbsoluteTime.addDuration(
|
||||||
|
AbsoluteTime.now(),
|
||||||
|
Duration.fromSpec({ days: 2 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
||||||
|
hContractTerms,
|
||||||
|
mergePub: mergePair.pub,
|
||||||
|
minAge: 0,
|
||||||
|
purseAmount: Amounts.stringify(instructedAmount),
|
||||||
|
purseExpiration,
|
||||||
|
pursePriv: pursePair.priv,
|
||||||
|
});
|
||||||
|
|
||||||
|
const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
|
||||||
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
|
pursePub: pursePair.pub,
|
||||||
|
coins: coinSelRes.coins,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPurseUrl = new URL(
|
||||||
|
`purses/${pursePair.pub}/create`,
|
||||||
|
coinSelRes.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpResp = await ws.http.postJson(createPurseUrl.href, {
|
||||||
|
amount: Amounts.stringify(instructedAmount),
|
||||||
|
merge_pub: mergePair.pub,
|
||||||
|
purse_sig: purseSigResp.sig,
|
||||||
|
h_contract_terms: hContractTerms,
|
||||||
|
purse_expiration: purseExpiration,
|
||||||
|
deposits: depositSigsResp.deposits,
|
||||||
|
min_age: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await httpResp.json();
|
||||||
|
|
||||||
|
logger.info(`resp: ${j2s(resp)}`);
|
||||||
|
|
||||||
|
throw Error("not yet implemented");
|
||||||
|
}
|
@ -46,6 +46,8 @@ import {
|
|||||||
GetExchangeTosResult,
|
GetExchangeTosResult,
|
||||||
GetWithdrawalDetailsForAmountRequest,
|
GetWithdrawalDetailsForAmountRequest,
|
||||||
GetWithdrawalDetailsForUriRequest,
|
GetWithdrawalDetailsForUriRequest,
|
||||||
|
InitiatePeerPushPaymentRequest,
|
||||||
|
InitiatePeerPushPaymentResponse,
|
||||||
IntegrationTestArgs,
|
IntegrationTestArgs,
|
||||||
ManualWithdrawalDetails,
|
ManualWithdrawalDetails,
|
||||||
PreparePayRequest,
|
PreparePayRequest,
|
||||||
@ -118,6 +120,9 @@ export enum WalletApiOperation {
|
|||||||
ExportBackupPlain = "exportBackupPlain",
|
ExportBackupPlain = "exportBackupPlain",
|
||||||
WithdrawFakebank = "withdrawFakebank",
|
WithdrawFakebank = "withdrawFakebank",
|
||||||
ExportDb = "exportDb",
|
ExportDb = "exportDb",
|
||||||
|
InitiatePeerPushPayment = "initiatePeerPushPayment",
|
||||||
|
CheckPeerPushPayment = "checkPeerPushPayment",
|
||||||
|
AcceptPeerPushPayment = "acceptPeerPushPayment",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WalletOperations = {
|
export type WalletOperations = {
|
||||||
@ -277,6 +282,10 @@ export type WalletOperations = {
|
|||||||
request: {};
|
request: {};
|
||||||
response: any;
|
response: any;
|
||||||
};
|
};
|
||||||
|
[WalletApiOperation.InitiatePeerPushPayment]: {
|
||||||
|
request: InitiatePeerPushPaymentRequest;
|
||||||
|
response: InitiatePeerPushPaymentResponse;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestType<
|
export type RequestType<
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
codecForGetWithdrawalDetailsForAmountRequest,
|
codecForGetWithdrawalDetailsForAmountRequest,
|
||||||
codecForGetWithdrawalDetailsForUri,
|
codecForGetWithdrawalDetailsForUri,
|
||||||
codecForImportDbRequest,
|
codecForImportDbRequest,
|
||||||
|
codecForInitiatePeerPushPaymentRequest,
|
||||||
codecForIntegrationTestArgs,
|
codecForIntegrationTestArgs,
|
||||||
codecForListKnownBankAccounts,
|
codecForListKnownBankAccounts,
|
||||||
codecForPrepareDepositRequest,
|
codecForPrepareDepositRequest,
|
||||||
@ -143,6 +144,7 @@ import {
|
|||||||
processDownloadProposal,
|
processDownloadProposal,
|
||||||
processPurchasePay,
|
processPurchasePay,
|
||||||
} from "./operations/pay.js";
|
} from "./operations/pay.js";
|
||||||
|
import { 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 {
|
||||||
@ -1049,6 +1051,10 @@ async function dispatchRequestInternal(
|
|||||||
await importDb(ws.db.idbHandle(), req.dump);
|
await importDb(ws.db.idbHandle(), req.dump);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
case "initiatePeerPushPayment": {
|
||||||
|
const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
|
||||||
|
return await initiatePeerToPeerPush(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