diff options
author | Florian Dold <florian@dold.me> | 2022-07-12 17:41:14 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-07-12 17:41:14 +0200 |
commit | f11483b511ff1f839b9913c4832eee9109f67aeb (patch) | |
tree | 6f4e1c5891a24bbb7500cea3964d3826d2ef87e1 /packages/taler-wallet-core/src/operations | |
parent | b214934b75418d0d01c9556577d9594f1db5a319 (diff) | |
download | wallet-core-f11483b511ff1f839b9913c4832eee9109f67aeb.tar.xz |
wallet-core: implement accepting p2p push payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
3 files changed, 294 insertions, 30 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 3a9121502..e4eaf8913 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -16,22 +16,46 @@ import { AmountJson, - Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus, - BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms, - DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp, - WalletBackupContentV1 + Amounts, + BackupCoinSourceType, + BackupDenomSel, + BackupProposalStatus, + BackupPurchase, + BackupRefreshReason, + BackupRefundState, + codecForContractTerms, + DenomKeyType, + j2s, + Logger, + PayCoinSelection, + RefreshReason, + TalerProtocolTimestamp, + WalletBackupContentV1, } from "@gnu-taler/taler-util"; import { - AbortStatus, CoinSource, + AbortStatus, + CoinSource, CoinSourceType, - CoinStatus, DenominationVerificationStatus, DenomSelectionState, OperationStatus, ProposalDownload, - ProposalStatus, RefreshCoinStatus, RefreshSessionRecord, RefundState, ReserveBankInfo, - ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo + CoinStatus, + DenominationVerificationStatus, + DenomSelectionState, + OperationStatus, + ProposalDownload, + ProposalStatus, + RefreshCoinStatus, + RefreshSessionRecord, + RefundState, + ReserveBankInfo, + ReserveRecordStatus, + WalletContractData, + WalletRefundItem, + WalletStoresV1, + WireInfo, } from "../../db.js"; import { InternalWalletState } from "../../internal-wallet-state.js"; import { checkDbInvariant, - checkLogicInvariant + checkLogicInvariant, } from "../../util/invariants.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js"; import { RetryInfo } from "../../util/retries.js"; @@ -313,14 +337,12 @@ export async function importBackup( } for (const backupDenomination of backupExchangeDetails.denominations) { - if ( - backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa - ) { + if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) { throw Error("unsupported cipher"); } const denomPubHash = cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key + backupDenomination.denom_pub.rsa_public_key ]; checkLogicInvariant(!!denomPubHash); const existingDenom = await tx.denominations.get([ @@ -535,7 +557,7 @@ export async function importBackup( const amount = Amounts.parseOrThrow(parsedContractTerms.amount); const contractTermsHash = cryptoComp.proposalIdToContractTermsHash[ - backupProposal.proposal_id + backupProposal.proposal_id ]; let maxWireFee: AmountJson; if (parsedContractTerms.max_wire_fee) { @@ -679,7 +701,7 @@ export async function importBackup( const amount = Amounts.parseOrThrow(parsedContractTerms.amount); const contractTermsHash = cryptoComp.proposalIdToContractTermsHash[ - backupPurchase.proposal_id + backupPurchase.proposal_id ]; let maxWireFee: AmountJson; if (parsedContractTerms.max_wire_fee) { diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index b6bae7518..55b8f513d 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -35,6 +35,7 @@ import { ConfirmPayResult, ConfirmPayResultType, ContractTerms, + ContractTermsUtil, Duration, durationMax, durationMin, @@ -87,7 +88,6 @@ import { selectForcedPayCoins, selectPayCoins, } from "../util/coinSelection.js"; -import { ContractTermsUtil } from "../util/contractTerms.js"; import { getHttpResponseErrorDetails, readSuccessResponseJsonOrErrorCode, 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 e2ae1e66e..658cbe4f7 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -18,25 +18,47 @@ * Imports. */ import { + AbsoluteTime, + AcceptPeerPushPaymentRequest, AmountJson, Amounts, - Logger, - InitiatePeerPushPaymentResponse, + AmountString, + buildCodecForObject, + CheckPeerPushPaymentRequest, + CheckPeerPushPaymentResponse, + Codec, + codecForAmountString, + codecForAny, + codecForExchangeGetContractResponse, + ContractTermsUtil, + decodeCrock, + Duration, + eddsaGetPublic, + encodeCrock, + ExchangePurseMergeRequest, InitiatePeerPushPaymentRequest, - strcmp, - CoinPublicKeyString, + InitiatePeerPushPaymentResponse, j2s, - getRandomBytes, - Duration, - durationAdd, + Logger, + strcmp, TalerProtocolTimestamp, - AbsoluteTime, - encodeCrock, - AmountString, UnblindedSignature, + WalletAccountMergeFlags, } from "@gnu-taler/taler-util"; -import { CoinStatus } from "../db.js"; +import { url } from "inspector"; +import { + CoinStatus, + OperationStatus, + ReserveRecord, + ReserveRecordStatus, +} from "../db.js"; +import { + checkSuccessResponseOrThrow, + readSuccessResponseJsonOrThrow, + throwUnexpectedRequestError, +} from "../util/http.js"; import { InternalWalletState } from "../internal-wallet-state.js"; +import { checkDbInvariant } from "../util/invariants.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -176,14 +198,22 @@ export async function initiatePeerToPeerPush( const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - const hContractTerms = encodeCrock(getRandomBytes(64)); - const purseExpiration = AbsoluteTime.toTimestamp( + + const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ days: 2 }), ), ); + const contractTerms = { + ...req.partialContractTerms, + purse_expiration: purseExpiration, + amount: req.amount, + }; + + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + const purseSigResp = await ws.cryptoApi.signPurseCreation({ hContractTerms, mergePub: mergePair.pub, @@ -204,6 +234,13 @@ export async function initiatePeerToPeerPush( coinSelRes.exchangeBaseUrl, ); + const econtractResp = await ws.cryptoApi.encryptContractForMerge({ + contractTerms, + mergePriv: mergePair.priv, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + }); + const httpResp = await ws.http.postJson(createPurseUrl.href, { amount: Amounts.stringify(instructedAmount), merge_pub: mergePair.pub, @@ -212,11 +249,216 @@ export async function initiatePeerToPeerPush( purse_expiration: purseExpiration, deposits: depositSigsResp.deposits, min_age: 0, + econtract: econtractResp.econtract, }); const resp = await httpResp.json(); logger.info(`resp: ${j2s(resp)}`); - throw Error("not yet implemented"); + if (httpResp.status !== 200) { + throw Error("got error response from exchange"); + } + + return { + contractPriv: econtractResp.contractPriv, + mergePriv: mergePair.priv, + pursePub: pursePair.pub, + exchangeBaseUrl: coinSelRes.exchangeBaseUrl, + }; +} + +interface ExchangePurseStatus { + balance: AmountString; +} + +export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> => + buildCodecForObject<ExchangePurseStatus>() + .property("balance", codecForAmountString()) + .build("ExchangePurseStatus"); + +export async function checkPeerPushPayment( + ws: InternalWalletState, + req: CheckPeerPushPaymentRequest, +): Promise<CheckPeerPushPaymentResponse> { + const getPurseUrl = new URL( + `purses/${req.pursePub}/deposit`, + req.exchangeBaseUrl, + ); + + const contractPub = encodeCrock( + eddsaGetPublic(decodeCrock(req.contractPriv)), + ); + + const purseHttpResp = await ws.http.get(getPurseUrl.href); + + const purseStatus = await readSuccessResponseJsonOrThrow( + purseHttpResp, + codecForExchangePurseStatus(), + ); + + const getContractUrl = new URL( + `contracts/${contractPub}`, + req.exchangeBaseUrl, + ); + + const contractHttpResp = await ws.http.get(getContractUrl.href); + + const contractResp = await readSuccessResponseJsonOrThrow( + contractHttpResp, + codecForExchangeGetContractResponse(), + ); + + const dec = await ws.cryptoApi.decryptContractForMerge({ + ciphertext: contractResp.econtract, + contractPriv: req.contractPriv, + pursePub: req.pursePub, + }); + + await ws.db + .mktx((x) => ({ + peerPushPaymentIncoming: x.peerPushPaymentIncoming, + })) + .runReadWrite(async (tx) => { + await tx.peerPushPaymentIncoming.add({ + contractPriv: req.contractPriv, + exchangeBaseUrl: req.exchangeBaseUrl, + mergePriv: dec.mergePriv, + pursePub: req.pursePub, + timestampAccepted: TalerProtocolTimestamp.now(), + contractTerms: dec.contractTerms, + }); + }); + + return { + amount: purseStatus.balance, + contractTerms: dec.contractTerms, + }; +} + +export function talerPaytoFromExchangeReserve( + exchangeBaseUrl: string, + reservePub: string, +): string { + const url = new URL(exchangeBaseUrl); + let proto: string; + if (url.protocol === "http:") { + proto = "taler+http"; + } else if (url.protocol === "https:") { + proto = "taler"; + } else { + throw Error(`unsupported exchange base URL protocol (${url.protocol})`); + } + + let path = url.pathname; + if (!path.endsWith("/")) { + path = path + "/"; + } + + return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; +} + +export async function acceptPeerPushPayment( + ws: InternalWalletState, + req: AcceptPeerPushPaymentRequest, +) { + const peerInc = await ws.db + .mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming })) + .runReadOnly(async (tx) => { + return tx.peerPushPaymentIncoming.get([ + req.exchangeBaseUrl, + req.pursePub, + ]); + }); + + if (!peerInc) { + throw Error("can't accept unknown incoming p2p push payment"); + } + + const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount); + + // We have to create the key pair outside of the transaction, + // due to the async crypto API. + const newReservePair = await ws.cryptoApi.createEddsaKeypair({}); + + const reserve: ReserveRecord | undefined = await ws.db + .mktx((x) => ({ + exchanges: x.exchanges, + reserves: x.reserves, + })) + .runReadWrite(async (tx) => { + const ex = await tx.exchanges.get(req.exchangeBaseUrl); + checkDbInvariant(!!ex); + if (ex.currentMergeReservePub) { + return await tx.reserves.get(ex.currentMergeReservePub); + } + const rec: ReserveRecord = { + exchangeBaseUrl: req.exchangeBaseUrl, + // FIXME: field will be removed in the future, folded into withdrawal/p2p record. + reserveStatus: ReserveRecordStatus.Dormant, + timestampCreated: TalerProtocolTimestamp.now(), + instructedAmount: Amounts.getZero(amount.currency), + currency: amount.currency, + reservePub: newReservePair.pub, + reservePriv: newReservePair.priv, + timestampBankConfirmed: undefined, + timestampReserveInfoPosted: undefined, + // FIXME! + initialDenomSel: undefined as any, + // FIXME! + initialWithdrawalGroupId: "", + initialWithdrawalStarted: false, + lastError: undefined, + operationStatus: OperationStatus.Pending, + retryInfo: undefined, + bankInfo: undefined, + restrictAge: undefined, + senderWire: undefined, + }; + await tx.reserves.put(rec); + return rec; + }); + + if (!reserve) { + throw Error("can't create reserve"); + } + + const mergeTimestamp = TalerProtocolTimestamp.now(); + + const reservePayto = talerPaytoFromExchangeReserve( + reserve.exchangeBaseUrl, + reserve.reservePub, + ); + + const sigRes = await ws.cryptoApi.signPurseMerge({ + contractTermsHash: ContractTermsUtil.hashContractTerms( + peerInc.contractTerms, + ), + flags: WalletAccountMergeFlags.MergeFullyPaidPurse, + mergePriv: peerInc.mergePriv, + mergeTimestamp: mergeTimestamp, + purseAmount: Amounts.stringify(amount), + purseExpiration: peerInc.contractTerms.purse_expiration, + purseFee: Amounts.stringify(Amounts.getZero(amount.currency)), + pursePub: peerInc.pursePub, + reservePayto, + reservePriv: reserve.reservePriv, + }); + + const mergePurseUrl = new URL( + `purses/${req.pursePub}/merge`, + req.exchangeBaseUrl, + ); + + const mergeReq: ExchangePurseMergeRequest = { + payto_uri: reservePayto, + merge_timestamp: mergeTimestamp, + merge_sig: sigRes.mergeSig, + reserve_sig: sigRes.accountSig, + }; + + const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq); + + const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny()); + logger.info(`merge result: ${j2s(res)}`); } |