@test-org122/hypernet-core
Version:
Hypernet Core. Represents the SDK for running the Hypernet Protocol.
399 lines • 21.4 kB
JavaScript
"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