UNPKG

@test-org122/hypernet-core

Version:

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

530 lines (481 loc) 20 kB
import { NodeResponses } from "@connext/vector-types"; import { ResultUtils } from "@test-org122/utils"; import { IPaymentRepository } from "@interfaces/data/IPaymentRepository"; import { BigNumber, EthereumAddress, HypernetConfig, IHypernetOfferDetails, InitializedHypernetContext, Payment, PublicIdentifier, PublicKey, PullPayment, PushPayment, ResultAsync, } from "@interfaces/objects"; import { CoreUninitializedError, LogicalError, PaymentFinalizeError, PaymentStakeError, RouterChannelUnknownError, TransferResolutionError, VectorError, } from "@interfaces/objects/errors"; import { IHypernetPullPaymentDetails } from "@interfaces/objects/HypernetPullPaymentDetails"; import { IRate } from "@interfaces/objects/Rate"; import { EPaymentType, ETransferType, MessageState } from "@interfaces/types"; import { EMessageTransferType } from "@interfaces/types/EMessageTransferType"; import { IBasicTransferResponse, IBrowserNode, IBrowserNodeProvider, IConfigProvider, IContextProvider, IFullTransferState, ILogUtils, IPaymentUtils, ITimeUtils, IVectorUtils, } from "@interfaces/utilities"; import { combine, errAsync, okAsync } from "neverthrow"; /** * Contains methods for creating push, pull, etc payments, * as well as retrieving them, and finalizing them. */ export class PaymentRepository implements IPaymentRepository { /** * Returns an instance of PaymentRepository */ constructor( protected browserNodeProvider: IBrowserNodeProvider, protected vectorUtils: IVectorUtils, protected configProvider: IConfigProvider, protected contextProvider: IContextProvider, protected paymentUtils: IPaymentUtils, protected logUtils: ILogUtils, protected timeUtils: ITimeUtils, ) {} public createPullRecord( paymentId: string, amount: string, ): ResultAsync<Payment, RouterChannelUnknownError | CoreUninitializedError | VectorError | Error> { let transfers: IFullTransferState[]; let browserNode: IBrowserNode; return ResultUtils.combine([this._getTransfersByPaymentId(paymentId), this.browserNodeProvider.getBrowserNode()]) .andThen((vals) => { [transfers, browserNode] = vals; return this.paymentUtils.transfersToPayment(paymentId, transfers); }) .andThen((payment) => { const message: IHypernetPullPaymentDetails = { messageType: EMessageTransferType.PULLPAYMENT, paymentId: paymentId, to: payment.to, from: payment.from, paymentToken: payment.paymentToken, pullPaymentAmount: amount, }; return this.vectorUtils.createPullNotificationTransfer(payment.to, message); }) .andThen((transferResponse) => { // Get the newly minted transfer return browserNode.getTransfer(transferResponse.transferId); }) .andThen((newTransfer) => { // Add the new transfer to the list transfers.push(newTransfer); // Convert the list of transfers to a payment (again) return this.paymentUtils.transfersToPayment(paymentId, transfers); }); } public createPullPayment( counterPartyAccount: PublicIdentifier, maximumAmount: string, // TODO: amounts should be consistently use BigNumber deltaTime: number, deltaAmount: string, // TODO: amounts should be consistently use BigNumber expirationDate: number, requiredStake: string, // TODO: amounts should be consistently use BigNumber paymentToken: EthereumAddress, merchantUrl: string, ): ResultAsync<Payment, RouterChannelUnknownError | CoreUninitializedError | VectorError | Error> { let browserNode: IBrowserNode; let context: InitializedHypernetContext; let paymentId: string; return ResultUtils.combine([ this.browserNodeProvider.getBrowserNode(), this.contextProvider.getInitializedContext(), this.paymentUtils.createPaymentId(EPaymentType.Pull), ]) .andThen((vals) => { [browserNode, context, paymentId] = vals; const message: IHypernetOfferDetails = { messageType: EMessageTransferType.OFFER, paymentId, creationDate: this.timeUtils.getUnixNow(), to: counterPartyAccount, from: context.publicIdentifier, requiredStake, paymentAmount: maximumAmount, expirationDate, paymentToken, merchantUrl, rate: { deltaAmount, deltaTime, }, }; // Create a message transfer, with the terms of the payment in the metadata. return this.vectorUtils.createOfferTransfer(counterPartyAccount, message); }) .andThen((transferInfo) => { return browserNode.getTransfer(transferInfo.transferId); }) .andThen((transfer) => { // Return the payment return this.paymentUtils.transfersToPayment(paymentId, [transfer]); }); } /** * Creates a push payment and returns it. Nothing moves until * the payment is accepted; the payment will return with the * "PROPOSED" status. This function just creates an OfferTransfer. * @param counterPartyAccount the public identifier of the account to pay * @param amount the amount to pay the counterparty * @param expirationDate the date (in unix time) at which point the payment will expire & revert * @param requiredStake the amount of insurance the counterparty must put up for this payment * @param paymentToken the (Ethereum) address of the payment token * @param merchantUrl the registered URL for the merchant that will resolve any disputes. */ public createPushPayment( counterPartyAccount: PublicIdentifier, amount: string, expirationDate: number, requiredStake: string, paymentToken: EthereumAddress, merchantUrl: string, ): ResultAsync<Payment, RouterChannelUnknownError | CoreUninitializedError | VectorError | Error> { let browserNode: IBrowserNode; let context: InitializedHypernetContext; let paymentId: string; return ResultUtils.combine([ this.browserNodeProvider.getBrowserNode(), this.contextProvider.getInitializedContext(), this.paymentUtils.createPaymentId(EPaymentType.Push), ]) .andThen((vals) => { [browserNode, context, paymentId] = vals; const message: IHypernetOfferDetails = { messageType: EMessageTransferType.OFFER, paymentId, creationDate: this.timeUtils.getUnixNow(), to: counterPartyAccount, from: context.publicIdentifier, requiredStake: requiredStake.toString(), paymentAmount: amount.toString(), expirationDate: expirationDate, paymentToken, merchantUrl, }; // Create a message transfer, with the terms of the payment in the metadata. return this.vectorUtils.createOfferTransfer(counterPartyAccount, message); }) .andThen((transferInfo) => { return browserNode.getTransfer(transferInfo.transferId); }) .andThen((transfer) => { // Return the payment return this.paymentUtils.transfersToPayment(paymentId, [transfer]); }); } /** * Given a paymentId, return the component transfers. * @param paymentId the payment to get transfers for */ protected _getTransfersByPaymentId(paymentId: string): ResultAsync<IFullTransferState[], Error> { let browserNode: IBrowserNode; let channelAddress: string; return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this.vectorUtils.getRouterChannelAddress()]) .andThen((vals) => { [browserNode, channelAddress] = vals; return browserNode.getActiveTransfers(channelAddress); }) .andThen((activeTransfers) => { // We also need to look for potentially resolved transfers const earliestDate = this.paymentUtils.getEarliestDateFromTransfers(activeTransfers); return browserNode.getTransfers(earliestDate, this.timeUtils.getUnixNow()); }) .andThen((transfers) => { // This new list is complete- it should include active and inactive transfers // after the earliest active transfer const transferTypeResults = new Array< ResultAsync< { transferType: ETransferType; transfer: IFullTransferState; }, VectorError | Error > >(); for (const transfer of transfers) { transferTypeResults.push(this.paymentUtils.getTransferTypeWithTransfer(transfer)); } return combine(transferTypeResults); }) .andThen((tranferTypesWithTransfers) => { // For each transfer, we are either just going to know it's relevant // from the data in the metadata, or we are going to check if it's an // insurance payment and we have more bulletproof ways to check const relevantTransfers: IFullTransferState[] = []; for (const transferTypeWithTransfer of tranferTypesWithTransfers) { const { transferType, transfer } = transferTypeWithTransfer; if (transferType === ETransferType.Offer) { const offerDetails: IHypernetOfferDetails = JSON.parse((transfer.transferState as MessageState).message); if (offerDetails.paymentId === paymentId) { relevantTransfers.push(transfer); } } else { if (transferType === ETransferType.Insurance || transferType === ETransferType.Parameterized) { if (paymentId === transfer.transferState.UUID) { relevantTransfers.push(transfer); } else { this.logUtils.log(`Transfer not relevant in PaymentRepository, transferId: ${transfer.transferId}`); } } else { this.logUtils.log(`Unrecognized transfer in PaymentRepository, transferId: ${transfer.transferId}`); } } } return okAsync(relevantTransfers); }); } /** * Given a list of payment Ids, return the associated payments. * @param paymentIds the list of payments to get */ public getPaymentsByIds(paymentIds: string[]): ResultAsync<Map<string, Payment>, Error> { let browserNode: IBrowserNode; let channelAddress: string; return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this.vectorUtils.getRouterChannelAddress()]) .andThen((vals) => { [browserNode, channelAddress] = vals; return browserNode.getActiveTransfers(channelAddress); }) .andThen((activeTransfers) => { // We also need to look for potentially resolved transfers const earliestDate = this.paymentUtils.getEarliestDateFromTransfers(activeTransfers); return browserNode.getTransfers(earliestDate, this.timeUtils.getUnixNow()); }) .andThen((transfers) => { const transferTypeResults = new Array< ResultAsync< { transferType: ETransferType; transfer: IFullTransferState; }, VectorError | Error > >(); for (const transfer of transfers) { transferTypeResults.push(this.paymentUtils.getTransferTypeWithTransfer(transfer)); } return combine(transferTypeResults); }) .andThen((tranferTypesWithTransfers) => { // For each transfer, we are either just going to know it's relevant // from the data in the metadata, or we are going to check if it's an // insurance payment and we have more bulletproof ways to check const relevantTransfers: IFullTransferState[] = []; for (const transferTypeWithTransfer of tranferTypesWithTransfers) { const { transferType, transfer } = transferTypeWithTransfer; if (transferType === ETransferType.Offer) { const offerDetails: IHypernetOfferDetails = JSON.parse((transfer.transferState as MessageState).message); if (paymentIds.includes(offerDetails.paymentId)) { relevantTransfers.push(transfer); } } else { if (transferType === ETransferType.Insurance || transferType === ETransferType.Parameterized) { if (paymentIds.includes(transfer.transferState.UUID)) { relevantTransfers.push(transfer); } else { this.logUtils.log(`Transfer not relevant in PaymentRepository, transferId: ${transfer.transferId}`); } } else { this.logUtils.log(`Unrecognized transfer in PaymentRepository, transferId: ${transfer.transferId}`); } } } return this.paymentUtils.transfersToPayments(relevantTransfers); }) .map((payments) => { return payments.reduce((map, obj) => { map.set(obj.id, obj); return map; }, new Map<string, Payment>()); }); } /** * Finalizes/confirms a payment * Internally, this is what actually calls resolve() on the Vector transfer - * be it a insurancePayments or parameterizedPayments. * @param paymentId the payment to finalize * @param amount the amount of the payment to finalize for */ public finalizePayment( paymentId: string, amount: string, ): ResultAsync<Payment, RouterChannelUnknownError | CoreUninitializedError | VectorError | Error> { let browserNode: IBrowserNode; let existingTransfers: IFullTransferState[]; let parameterizedTransferId: string; return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this._getTransfersByPaymentId(paymentId)]) .andThen((vals) => { [browserNode, existingTransfers] = vals; this.logUtils.log(`Finalizing payment ${paymentId}`); // get the transfer id from the paymentId // use payment utils for this return this.paymentUtils.sortTransfers(paymentId, existingTransfers); }) .andThen((sortedTransfers) => { if (sortedTransfers.parameterizedTransfer == null) { return errAsync( new PaymentFinalizeError( `Cannot finalize payment ${paymentId}, no parameterized transfer exists for this!`, ), ); } parameterizedTransferId = sortedTransfers.parameterizedTransfer.transferId; return this.vectorUtils.resolvePaymentTransfer(parameterizedTransferId, paymentId, amount); }) .andThen(() => { return browserNode.getTransfer(parameterizedTransferId); }) .andThen((transfer) => { // Remove the parameterized transfer, and replace it // with this latest transfer existingTransfers = existingTransfers.filter((obj) => obj.transferId !== parameterizedTransferId); existingTransfers.push(transfer); // Transfer has been resolved successfully; return the updated payment. const updatedPayment = this.paymentUtils.transfersToPayment(paymentId, existingTransfers); return updatedPayment; }); } /** * Provides stake for a given payment id * Internally, this is what actually creates the InsurancePayments with Vector. * @param paymentId the payment for which to provide stake for */ public provideStake( paymentId: string, merchantPublicKey: PublicKey, ): ResultAsync< Payment, | PaymentStakeError | TransferResolutionError | RouterChannelUnknownError | CoreUninitializedError | VectorError | Error > { let browserNode: IBrowserNode; let config: HypernetConfig; let existingTransfers: IFullTransferState[]; return ResultUtils.combine([ this.browserNodeProvider.getBrowserNode(), this.configProvider.getConfig(), this._getTransfersByPaymentId(paymentId), ]) .andThen((vals) => { [browserNode, config, existingTransfers] = vals; return this.paymentUtils.transfersToPayment(paymentId, existingTransfers); }) .andThen((payment) => { const paymentSender = payment.from; const paymentID = payment.id; const paymentStart = this.timeUtils.getUnixNow(); const paymentExpiration = paymentStart + config.defaultPaymentExpiryLength; // TODO: There are probably some logical times when you should not provide a stake if (false) { return errAsync(new PaymentStakeError()); } this.logUtils.log(`PaymentRepository:provideStake: Creating insurance transfer for paymentId: ${paymentId}`); return this.vectorUtils.createInsuranceTransfer( paymentSender, merchantPublicKey, payment.requiredStake, paymentExpiration, paymentID, ); }) .andThen((transferInfoUnk) => { const transferInfo = transferInfoUnk as IBasicTransferResponse; return browserNode.getTransfer(transferInfo.transferId); }) .andThen((transfer) => { const allTransfers = [transfer, ...existingTransfers]; // Transfer has been created successfully; return the updated payment. return this.paymentUtils.transfersToPayment(paymentId, allTransfers); }); } /** * Singular version of provideAssets * Internally, creates a parameterizedPayment with Vector, * and returns a payment of state 'Approved' * @param paymentId the payment for which to provide an asset for */ public provideAsset( paymentId: string, ): ResultAsync<Payment, RouterChannelUnknownError | CoreUninitializedError | VectorError | LogicalError> { let browserNode: IBrowserNode; let config: HypernetConfig; let existingTransfers: IFullTransferState[]; return ResultUtils.combine([ this.browserNodeProvider.getBrowserNode(), this.configProvider.getConfig(), this._getTransfersByPaymentId(paymentId), ]) .andThen((vals) => { [browserNode, config, existingTransfers] = vals; return this.paymentUtils.transfersToPayment(paymentId, existingTransfers); }) .andThen((payment) => { const paymentTokenAddress = payment.paymentToken; let paymentTokenAmount: BigNumber; if (payment instanceof PushPayment) { paymentTokenAmount = payment.paymentAmount; } else if (payment instanceof PullPayment) { paymentTokenAmount = payment.authorizedAmount; } else { this.logUtils.error(`Payment was not instance of push or pull payment!`); return errAsync(new LogicalError()); } const paymentRecipient = payment.to; const paymentID = payment.id; const paymentStart = this.timeUtils.getUnixNow(); const paymentExpiration = paymentStart + config.defaultPaymentExpiryLength; this.logUtils.log(`Providing a payment amount of ${paymentTokenAmount}`); // Use vectorUtils to create the parameterizedPayment return this.vectorUtils.createPaymentTransfer( payment instanceof PushPayment ? EPaymentType.Push : EPaymentType.Pull, paymentRecipient, paymentTokenAmount, paymentTokenAddress, paymentID, paymentStart, paymentExpiration, payment instanceof PullPayment ? payment.deltaTime : undefined, payment instanceof PullPayment ? payment.deltaAmount.toString() : undefined, ); }) .andThen((transferInfoUnk) => { const transferInfo = transferInfoUnk as NodeResponses.ConditionalTransfer; return browserNode.getTransfer(transferInfo.transferId); }) .andThen((transfer) => { const allTransfers = [transfer, ...existingTransfers]; // Transfer has been created successfully; return the updated payment. return this.paymentUtils.transfersToPayment(paymentId, allTransfers); }); } }