UNPKG

@test-org122/hypernet-core

Version:

Hypernet Core. Represents the SDK for running the Hypernet Protocol.

399 lines 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PaymentService = void 0; const utils_1 = require("@test-org122/utils"); const objects_1 = require("@interfaces/objects"); const errors_1 = require("@interfaces/objects/errors"); const types_1 = require("@interfaces/types"); const neverthrow_1 = require("neverthrow"); /** * PaymentService uses Vector internally to send payments on the requested channel. * The order of operations for sending funds is as follows: * sendFunds() is called by sender, which creates a Message transfer with Vector, which triggers * offerReceived() on the recipient side, which tosses an event up to the user, who then calls * acceptOffers() to accept the sender's funds, which creates an Insurance transfer with Vector, which triggers * stakePosted() on the sender's side, which finally creates the Parameterized transfer with Vector, which triggers * paymentPosted() on the recipient's side, which finalizes/resolves the vector parameterized transfer. * * Note that the general expected order of operations is mirrored by the ordering of functions within this class. * * @todo we should also finalize the insurance transfer, and maybe finalize the offer transfer */ class PaymentService { /** * Creates an instanceo of the paymentService. */ constructor(linkRepository, accountRepository, contextProvider, configProvider, paymentRepository, merchantConnectorRepository, logUtils) { this.linkRepository = linkRepository; this.accountRepository = accountRepository; this.contextProvider = contextProvider; this.configProvider = configProvider; this.paymentRepository = paymentRepository; this.merchantConnectorRepository = merchantConnectorRepository; this.logUtils = logUtils; } /** * Authorizes funds to a specified counterparty, with an amount, rate, & expiration date. * @param counterPartyAccount the public identifier of the counterparty to authorize funds to * @param totalAuthorized the total amount the counterparty is allowed to "pull" * @param expirationDate the latest time in which the counterparty can pull funds. This must be after the full maturation date of totalAuthorized, as calculated via deltaAmount and deltaTime. * @param deltaAmount The amount per deltaTime to authorize * @param deltaTime the number of seconds after which deltaAmount will be authorized, up to the limit of totalAuthorized. * @param requiredStake the amount of stake the counterparyt must put up as insurance * @param paymentToken the (Ethereum) address of the payment token * @param merchantUrl the registered URL for the merchant that will resolve any disputes. */ authorizeFunds(counterPartyAccount, totalAuthorized, expirationDate, deltaAmount, deltaTime, requiredStake, paymentToken, merchantUrl) { // @TODO Check deltaAmount, deltaTime, totalAuthorized, and expiration date // totalAuthorized / (deltaAmount/deltaTime) > ((expiration date - now) + someMinimumNumDays) return this.paymentRepository.createPullPayment(counterPartyAccount, totalAuthorized.toString(), deltaTime, deltaAmount, expirationDate, requiredStake.toString(), paymentToken, merchantUrl); } pullFunds(paymentId, amount) { // Pull the up the payment return this.paymentRepository.getPaymentsByIds([paymentId]).andThen((payments) => { const payment = payments.get(paymentId); // Verify that it is indeed a pull payment if (payment instanceof objects_1.PullPayment) { // Verify that we're not pulling too quickly (greater than the average rate) if (payment.amountTransferred.add(amount).gt(payment.vestedAmount)) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError(`Amount of ${amount} exceeds the vested payment amount of ${payment.vestedAmount}`)); } // Verify that the amount we're trying to pull does not exceed the total authorized amount if (payment.amountTransferred.add(amount).gt(payment.authorizedAmount)) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError(`Amount of ${amount} exceeds the total authorized amount of ${payment.authorizedAmount}`)); } // Create the PullRecord return this.paymentRepository.createPullRecord(paymentId, amount.toString()); } else { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Can not pull funds from a non pull payment")); } }); } /** * Sends a payment to the specified recipient. * Internally, creates a null/message/offer transfer to communicate * with the counterparty and signal a request for a stake. * @param counterPartyAccount the intended recipient * @param amount the amount of payment to send * @param expirationDate the expiration date at which point this payment will revert * @param requiredStake the amount of insurance the counterparty should put up * @param paymentToken the (Ethereum) address of the payment token * @param merchantUrl the registered URL for the merchant that will resolve any disputes. */ sendFunds(counterPartyAccount, amount, expirationDate, requiredStake, paymentToken, merchantUrl) { // TODO: Sanity checking on the values return this.paymentRepository.createPushPayment(counterPartyAccount, amount, expirationDate, requiredStake, paymentToken, merchantUrl); } /** * Called when someone has sent us a payment offer. * Lookup the transfer, and convert it to a payment. * Then, publish an RXJS event to the user. * @param paymentId the paymentId for the offer */ offerReceived(paymentId) { const prerequisites = utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getInitializedContext(), ]); return prerequisites.andThen((vals) => { const [payments, context] = vals; const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.LogicalError(`PaymentService:offerReceived():Could not get payment!`)); } if (payment.state !== types_1.EPaymentState.Proposed) { // The payment has already moved forward, somehow. // We don't need to do anything, we probably got called // by another instance of the core. return neverthrow_1.okAsync(undefined); } // Payment state is 'Proposed', continue to handle if (payment instanceof objects_1.PushPayment) { // Someone wants to send us a pushPayment, emit up to the api context.onPushPaymentProposed.next(payment); } else if (payment instanceof objects_1.PullPayment) { // Someone wants to send us a pullPayment, emit up to the api context.onPullPaymentProposed.next(payment); } else { throw new Error("Unknown payment type!"); } return neverthrow_1.okAsync(undefined); }); } /** * For each paymentID provided, attempts to accept funds (ie: provide a stake) for that payment. * @param paymentIds a list of paymentIds for which to accept funds for */ acceptOffers(paymentIds) { let config; let payments; const merchantUrls = new Set(); return utils_1.ResultUtils.combine([this.configProvider.getConfig(), this.paymentRepository.getPaymentsByIds(paymentIds)]) .andThen((vals) => { [config, payments] = vals; // Iterate over the payments, and find all the merchant URLs. for (const keyval of payments) { merchantUrls.add(keyval[1].merchantUrl); } return utils_1.ResultUtils.combine([ this.accountRepository.getBalanceByAsset(config.hypertokenAddress), this.merchantConnectorRepository.getMerchantPublicKeys(Array.from(merchantUrls)), ]); }) .andThen((vals) => { const [hypertokenBalance, publicKeys] = vals; // If we don't have a public key for each merchant, then we should not proceed. if (merchantUrls.size != publicKeys.size) { return neverthrow_1.errAsync(new errors_1.MerchantValidationError("Not all merchants are authorized!")); } // For each payment ID, call the singular version of acceptOffers // Wrap each one as a Result object, and return an array of Results let totalStakeRequired = objects_1.BigNumber.from(0); // First, verify to make sure that we have enough hypertoken to cover the insurance collectively for (const [key, payment] of payments) { if (payment.state !== types_1.EPaymentState.Proposed) { return neverthrow_1.errAsync(new errors_1.AcceptPaymentError(`Cannot accept payment ${payment.id}, it is not in the Proposed state`)); } totalStakeRequired = totalStakeRequired.add(objects_1.BigNumber.from(payment.requiredStake)); } // Check the balance and make sure you have enough HyperToken to cover it if (hypertokenBalance.freeAmount.lt(totalStakeRequired)) { return neverthrow_1.errAsync(new errors_1.InsufficientBalanceError("Not enough Hypertoken to cover provided payments.")); } // Now that we know we can (probably) make the payments, let's try const stakeAttempts = new Array(); for (const keyval of payments) { const [paymentId, payment] = keyval; this.logUtils.log(`PaymentService:acceptOffers: attempting to provide stake for payment ${paymentId}`); // We need to get the public key of the merchant for the payment const merchantPublicKey = publicKeys.get(payment.merchantUrl); if (merchantPublicKey != null) { const stakeAttempt = this.paymentRepository.provideStake(paymentId, merchantPublicKey).match((payment) => { return neverthrow_1.ok(payment); }, (e) => { return neverthrow_1.err(new errors_1.AcceptPaymentError(`Payment ${paymentId} could not be staked! Source exception: ${e}`)); }); stakeAttempts.push(stakeAttempt); } else { throw new Error("Merchant does not have a public key; are they "); } } return objects_1.ResultAsync.fromPromise(Promise.all(stakeAttempts), (e) => e); }); } /** * Notifies the service that a stake has been posted; if verified, * then provides assets to the counterparty (ie a parameterizedPayment) * @param paymentId the paymentId for the stake */ stakePosted(paymentId) { const prerequisites = utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getInitializedContext(), ]); let payments; let context; return prerequisites.andThen((vals) => { [payments, context] = vals; const payment = payments.get(paymentId); this.logUtils.log(`${paymentId}: ${JSON.stringify(payment)}`); if (payment == null) { this.logUtils.error(`Invalid payment ID: ${paymentId}`); return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID!")); } // Let the UI know we got an insurance transfer if (payment instanceof objects_1.PushPayment) { context.onPushPaymentUpdated.next(payment); } if (payment instanceof objects_1.PullPayment) { context.onPullPaymentUpdated.next(payment); } // Notified the UI, move on to advancing the state of the payment. // Payment state must be in "staked" in order to progress if (payment.state !== types_1.EPaymentState.Staked) { this.logUtils.error(`Invalid payment ${paymentId}, it must be in the staked status. Cannot provide payment!`); return neverthrow_1.errAsync(new errors_1.InvalidParametersError(`Invalid payment ${paymentId}, it must be in the staked status. Cannot provide payment!`)); } // If we created the stake, we can ignore this if (payment.from !== context.publicIdentifier) { this.logUtils.log("Not providing asset since payment is not from us!"); return neverthrow_1.okAsync(undefined); } // If the payment state is staked, we know that the proper // insurance has been posted. this.logUtils.log(`Providing asset for paymentId ${paymentId}`); return this.paymentRepository.provideAsset(paymentId).andThen((updatedPayment) => { if (updatedPayment instanceof objects_1.PushPayment) { this.logUtils.log("Providing asset for pushpayment."); context.onPushPaymentUpdated.next(updatedPayment); } if (updatedPayment instanceof objects_1.PullPayment) { this.logUtils.log("Providing asset for pullpayment."); context.onPullPaymentUpdated.next(updatedPayment); } return neverthrow_1.okAsync(undefined); }); }); } /** * Notifies the service that the parameterized payment has been created. * Called by the reciever of a parameterized transfer, AFTER they have put up stake, * and after the sender has created the Parameterized transfer * @param paymentId the payment ID to accept/resolve */ paymentPosted(paymentId) { const prerequisites = utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getInitializedContext(), ]); let payments; let context; return prerequisites.andThen((vals) => { [payments, context] = vals; const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID!")); } // Payment state must be in "approved" to finalize if (payment.state !== types_1.EPaymentState.Approved) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError(`Invalid payment ${paymentId}, it must be in the approved status. Cannot provide payment!`)); } // Make sure the stake is legit // if (!payment.requiredStake.eq(payment.amountStaked)) { // // TODO: We should be doing more here. The whole payment should be aborted. // return errAsync(new OfferMismatchError(`Invalid stake provided for payment ${paymentId}`)); // } // If we're the ones that *sent* the payment, we can ignore this if (payment.from === context.publicIdentifier) { this.logUtils.log("Doing nothing in paymentPosted because we are the ones that posted the payment!"); return neverthrow_1.okAsync(undefined); } // If the payment state is approved, we know that it matches our insurance payment if (payment instanceof objects_1.PushPayment) { // Resolve the parameterized payment immediately for the full balnce return this.paymentRepository .finalizePayment(paymentId, payment.paymentAmount.toString()) .map((finalizedPayment) => { context.onPushPaymentUpdated.next(finalizedPayment); }); } else if (payment instanceof objects_1.PullPayment) { // Notify the user that the funds have been approved. context.onPullPaymentApproved.next(payment); } return neverthrow_1.okAsync(undefined); }); } /** * Notifies the service that the parameterized payment has been resolved. * @param paymentId the payment id that has been resolved. */ paymentCompleted(paymentId) { let payments; let context; return utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getContext(), ]).andThen((vals) => { [payments, context] = vals; const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID!")); } // @todo add some additional checking here if (payment instanceof objects_1.PushPayment) { context.onPushPaymentUpdated.next(payment); } if (payment instanceof objects_1.PullPayment) { context.onPullPaymentUpdated.next(payment); } return neverthrow_1.okAsync(undefined); }); } /** * Right now, if the insurance is resolved, all we need to do is generate an update event. * * @param paymentId */ insuranceResolved(paymentId) { let payments; let context; return utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getContext(), ]).andThen((vals) => { [payments, context] = vals; const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID!")); } // @todo add some additional checking here if (payment instanceof objects_1.PushPayment) { context.onPushPaymentUpdated.next(payment); } if (payment instanceof objects_1.PullPayment) { context.onPullPaymentUpdated.next(payment); } return neverthrow_1.okAsync(undefined); }); } /** * Notifies the service that a pull-payment has been recorded. * @param paymentId the paymentId for the pull-payment */ pullRecorded(paymentId) { const prerequisites = utils_1.ResultUtils.combine([ this.paymentRepository.getPaymentsByIds([paymentId]), this.contextProvider.getContext(), ]); let payments; let context; return prerequisites.andThen((vals) => { [payments, context] = vals; const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID!")); } // Notify the world that this pull payment was updated if (payment instanceof objects_1.PullPayment) { context.onPullPaymentUpdated.next(payment); } return neverthrow_1.okAsync(undefined); }); } initiateDispute(paymentId) { // Get the payment return this.paymentRepository .getPaymentsByIds([paymentId]) .andThen((payments) => { const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID")); } // You can only dispute payments that are in the accepted state- the reciever has taken their money. // The second condition can't happen if it's in Accepted unless something is very, very badly wrong, // but it keeps typescript happy if (payment.state != types_1.EPaymentState.Accepted || payment.details.insuranceTransferId == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Can not dispute a payment that is not in the Accepted state")); } // Resolve the dispute return this.merchantConnectorRepository.resolveChallenge(payment.merchantUrl, paymentId, payment.details.insuranceTransferId); }) .andThen(() => { return this.paymentRepository.getPaymentsByIds([paymentId]); }) .andThen((payments) => { const payment = payments.get(paymentId); if (payment == null) { return neverthrow_1.errAsync(new errors_1.InvalidParametersError("Invalid payment ID")); } return neverthrow_1.okAsync(payment); }); } } exports.PaymentService = PaymentService; //# sourceMappingURL=PaymentService.js.map