diff options
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 140 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoTypes.ts | 66 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 65 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/peer-to-peer.ts | 164 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet-api-types.ts | 20 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 27 |
6 files changed, 442 insertions, 40 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 099bf09fe..2f39f7806 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -33,11 +33,11 @@ import { BlindedDenominationSignature, bufferForUint32, buildSigPS, - bytesToString, CoinDepositPermission, CoinEnvelope, createHashContext, decodeCrock, + decryptContractForDeposit, decryptContractForMerge, DenomKeyType, DepositInfo, @@ -47,6 +47,7 @@ import { eddsaSign, eddsaVerify, encodeCrock, + encryptContractForDeposit, encryptContractForMerge, ExchangeProtocolVersion, getRandomBytes, @@ -85,17 +86,23 @@ import { DenominationRecord } from "../db.js"; import { CreateRecoupRefreshReqRequest, CreateRecoupReqRequest, + DecryptContractForDepositRequest, + DecryptContractForDepositResponse, DecryptContractRequest, DecryptContractResponse, DerivedRefreshSession, DerivedTipPlanchet, DeriveRefreshSessionRequest, DeriveTipRequest, + EncryptContractForDepositRequest, + EncryptContractForDepositResponse, EncryptContractRequest, EncryptContractResponse, EncryptedContract, SignPurseMergeRequest, SignPurseMergeResponse, + SignReservePurseCreateRequest, + SignReservePurseCreateResponse, SignTrackTransactionRequest, } from "./cryptoTypes.js"; @@ -205,7 +212,19 @@ export interface TalerCryptoInterface { req: DecryptContractRequest, ): Promise<DecryptContractResponse>; + encryptContractForDeposit( + req: EncryptContractForDepositRequest, + ): Promise<EncryptContractForDepositResponse>; + + decryptContractForDeposit( + req: DecryptContractForDepositRequest, + ): Promise<DecryptContractForDepositResponse>; + signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>; + + signReservePurseCreate( + req: SignReservePurseCreateRequest, + ): Promise<SignReservePurseCreateResponse>; } /** @@ -362,6 +381,21 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise<SignPurseMergeResponse> { 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 @@ -1047,7 +1081,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { if (depositInfo.requiredMinimumAge != null) { s.minimum_age_sig = minimumAgeSig; - s.age_commitment = depositInfo.ageCommitmentProof?.commitment.publicKeys; + s.age_commitment = + depositInfo.ageCommitmentProof?.commitment.publicKeys; } else if (depositInfo.ageCommitmentProof) { (s as any).h_age_commitment = hAgeCommitment; } @@ -1389,6 +1424,43 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { 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( tci: TalerCryptoInterfaceR, req: SignPurseMergeRequest, @@ -1431,6 +1503,70 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { 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 { diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 6f4a5fa95..6e0e01627 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -187,6 +187,19 @@ export interface EncryptContractResponse { contractPriv: string; } +export interface EncryptContractForDepositRequest { + contractTerms: any; + + pursePub: string; + pursePriv: string; +} + +export interface EncryptContractForDepositResponse { + econtract: EncryptedContract; + + contractPriv: string; +} + export interface DecryptContractRequest { ciphertext: string; pursePub: string; @@ -198,6 +211,16 @@ export interface DecryptContractResponse { mergePriv: string; } +export interface DecryptContractForDepositRequest { + ciphertext: string; + pursePub: string; + contractPriv: string; +} + +export interface DecryptContractForDepositResponse { + contractTerms: any; +} + export interface SignPurseMergeRequest { mergeTimestamp: TalerProtocolTimestamp; @@ -227,6 +250,47 @@ export interface SignPurseMergeResponse { * Signature made by the purse's merge private key. */ mergeSig: 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; } diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a34a09f75..266197eb5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -393,7 +393,6 @@ export interface ExchangeDetailsRecord { wireInfo: WireInfo; } - export interface ExchangeDetailsPointer { masterPublicKey: string; @@ -922,7 +921,6 @@ export interface RefreshSessionRecord { norevealIndex?: number; } - export enum RefundState { Failed = "failed", Applied = "applied", @@ -1186,9 +1184,9 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState"; */ export type ConfigRecord = | { - key: typeof WALLET_BACKUP_STATE_KEY; - value: WalletBackupConfState; - } + key: typeof WALLET_BACKUP_STATE_KEY; + value: WalletBackupConfState; + } | { key: "currencyDefaultsApplied"; value: boolean }; export interface WalletBackupConfState { @@ -1405,17 +1403,17 @@ export enum BackupProviderStateTag { export type BackupProviderState = | { - tag: BackupProviderStateTag.Provisional; - } + tag: BackupProviderStateTag.Provisional; + } | { - tag: BackupProviderStateTag.Ready; - nextBackupTimestamp: TalerProtocolTimestamp; - } + tag: BackupProviderStateTag.Ready; + nextBackupTimestamp: TalerProtocolTimestamp; + } | { - tag: BackupProviderStateTag.Retrying; - retryInfo: RetryInfo; - lastError?: TalerErrorDetail; - }; + tag: BackupProviderStateTag.Retrying; + retryInfo: RetryInfo; + lastError?: TalerErrorDetail; + }; export interface BackupProviderTerms { supportedProtocolVersion: string; @@ -1625,6 +1623,36 @@ export interface PeerPushPaymentInitiationRecord { 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. * @@ -1825,6 +1853,15 @@ export const WalletStoresV1 = { ]), }, ), + peerPullPaymentInitiation: describeStore( + describeContents<PeerPullPaymentInitiationRecord>( + "peerPushPaymentInitiation", + { + keyPath: "pursePub", + }, + ), + {}, + ), }; export interface MetaConfigRecord { diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index 4d2f2bb5f..eca319a29 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -37,7 +37,10 @@ import { eddsaGetPublic, encodeCrock, ExchangePurseMergeRequest, + ExchangeReservePurseRequest, getRandomBytes, + InitiatePeerPullPaymentRequest, + InitiatePeerPullPaymentResponse, InitiatePeerPushPaymentRequest, InitiatePeerPushPaymentResponse, j2s, @@ -370,24 +373,12 @@ export function talerPaytoFromExchangeReserve( return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; } -export async function acceptPeerPushPayment( +async function getMergeReserveInfo( ws: InternalWalletState, - req: AcceptPeerPushPaymentRequest, -) { - const peerInc = await ws.db - .mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming })) - .runReadOnly(async (tx) => { - return tx.peerPushPaymentIncoming.get(req.peerPushPaymentIncomingId); - }); - - if (!peerInc) { - throw Error( - `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, - ); - } - - const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount); - + 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({}); @@ -398,7 +389,7 @@ export async function acceptPeerPushPayment( withdrawalGroups: x.withdrawalGroups, })) .runReadWrite(async (tx) => { - const ex = await tx.exchanges.get(peerInc.exchangeBaseUrl); + const ex = await tx.exchanges.get(req.exchangeBaseUrl); checkDbInvariant(!!ex); if (ex.currentMergeReserveInfo) { return ex.currentMergeReserveInfo; @@ -411,6 +402,31 @@ export async function acceptPeerPushPayment( return ex.currentMergeReserveInfo; }); + return mergeReserveInfo; +} + +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.peerPushPaymentIncomingId); + }); + + if (!peerInc) { + throw Error( + `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, + ); + } + + const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount); + + const mergeReserveInfo = await getMergeReserveInfo(ws, { + exchangeBaseUrl: peerInc.exchangeBaseUrl, + }); + const mergeTimestamp = TalerProtocolTimestamp.now(); const reservePayto = talerPaytoFromExchangeReserve( @@ -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, + }), + }; +} diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index cc9e98f8c..14c40a8db 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -27,6 +27,7 @@ import { AcceptExchangeTosRequest, AcceptManualWithdrawalRequest, AcceptManualWithdrawalResult, + AcceptPeerPullPaymentRequest, AcceptPeerPushPaymentRequest, AcceptTipRequest, AcceptWithdrawalResponse, @@ -35,6 +36,8 @@ import { ApplyRefundResponse, BackupRecovery, BalancesResponse, + CheckPeerPullPaymentRequest, + CheckPeerPullPaymentResponse, CheckPeerPushPaymentRequest, CheckPeerPushPaymentResponse, CoinDumpJson, @@ -49,6 +52,8 @@ import { GetExchangeTosResult, GetWithdrawalDetailsForAmountRequest, GetWithdrawalDetailsForUriRequest, + InitiatePeerPullPaymentRequest, + InitiatePeerPullPaymentResponse, InitiatePeerPushPaymentRequest, InitiatePeerPushPaymentResponse, IntegrationTestArgs, @@ -126,6 +131,9 @@ export enum WalletApiOperation { InitiatePeerPushPayment = "initiatePeerPushPayment", CheckPeerPushPayment = "checkPeerPushPayment", AcceptPeerPushPayment = "acceptPeerPushPayment", + InitiatePeerPullPayment = "initiatePeerPullPayment", + CheckPeerPullPayment = "checkPeerPullPayment", + AcceptPeerPullPayment = "acceptPeerPullPayment", } export type WalletOperations = { @@ -297,6 +305,18 @@ export type WalletOperations = { request: AcceptPeerPushPaymentRequest; response: {}; }; + [WalletApiOperation.InitiatePeerPullPayment]: { + request: InitiatePeerPullPaymentRequest; + response: InitiatePeerPullPaymentResponse; + }; + [WalletApiOperation.CheckPeerPullPayment]: { + request: CheckPeerPullPaymentRequest; + response: CheckPeerPullPaymentResponse; + }; + [WalletApiOperation.AcceptPeerPullPayment]: { + request: AcceptPeerPullPaymentRequest; + response: {}; + }; }; export type RequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 593d2e0ff..0d5918886 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -32,6 +32,7 @@ import { codecForAcceptBankIntegratedWithdrawalRequest, codecForAcceptExchangeTosRequest, codecForAcceptManualWithdrawalRequet, + codecForAcceptPeerPullPaymentRequest, codecForAcceptPeerPushPaymentRequest, codecForAcceptTipRequest, codecForAddExchangeRequest, @@ -50,6 +51,7 @@ import { codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, codecForImportDbRequest, + codecForInitiatePeerPullPaymentRequest, codecForInitiatePeerPushPaymentRequest, codecForIntegrationTestArgs, codecForListKnownBankAccounts, @@ -150,6 +152,7 @@ import { import { acceptPeerPushPayment, checkPeerPushPayment, + initiatePeerRequestForPay, initiatePeerToPeerPush, } from "./operations/peer-to-peer.js"; import { getPendingOperations } from "./operations/pending.js"; @@ -455,11 +458,20 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> { for (const c of builtinAuditors) { 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( @@ -568,8 +580,9 @@ async function getExchanges( continue; } - const denominations = await tx.denominations.indexes - .byExchangeBaseUrl.iter(r.baseUrl).toArray(); + const denominations = await tx.denominations.indexes.byExchangeBaseUrl + .iter(r.baseUrl) + .toArray(); if (!denominations) { continue; @@ -1030,6 +1043,10 @@ async function dispatchRequestInternal( await acceptPeerPushPayment(ws, req); return {}; } + case "initiatePeerPullPayment": { + const req = codecForInitiatePeerPullPaymentRequest().decode(payload); + return await initiatePeerRequestForPay(ws, req); + } } throw TalerError.fromDetail( TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, |