@test-org122/hypernet-core
Version:
Hypernet Core. Represents the SDK for running the Hypernet Protocol.
548 lines (491 loc) • 19.3 kB
text/typescript
import { DEFAULT_CHANNEL_TIMEOUT } from "@connext/vector-types";
import {
BigNumber,
HypernetConfig,
IHypernetOfferDetails,
InitializedHypernetContext,
PublicIdentifier,
PublicKey,
ResultAsync,
} from "@interfaces/objects";
import {
IBrowserNodeProvider,
IContextProvider,
IVectorUtils,
IConfigProvider,
IBlockchainProvider,
ILogUtils,
IPaymentIdUtils,
IBrowserNode,
IBasicTransferResponse,
IBasicChannelResponse,
IFullChannelState,
IFullTransferState,
ITimeUtils,
} from "@interfaces/utilities";
import {
EPaymentType,
ETransferState,
InsuranceResolver,
InsuranceResolverData,
InsuranceState,
MessageResolver,
MessageState,
ParameterizedState,
} from "@interfaces/types";
import "reflect-metadata";
import { serialize } from "class-transformer";
import { ParameterizedResolver, ParameterizedResolverData, Rate } from "@interfaces/types/typechain/ParameterizedTypes";
import { getSignerAddressFromPublicIdentifier } from "@connext/vector-utils/dist/identifiers";
import { defaultAbiCoder, keccak256 } from "ethers/lib/utils";
import {
CoreUninitializedError,
InvalidParametersError,
RouterChannelUnknownError,
RouterUnavailableError,
TransferCreationError,
TransferResolutionError,
VectorError,
} from "@interfaces/objects/errors";
import { errAsync, okAsync } from "neverthrow";
import { ResultUtils } from "@test-org122/utils";
import { IHypernetPullPaymentDetails } from "../../interfaces/objects/HypernetPullPaymentDetails";
import { EMessageTransferType } from "@interfaces/types/EMessageTransferType";
/**
* VectorUtils contains methods for interacting directly with the core Vector stuff -
* creating transfers, resolving them, & dealing the with router channel.
*/
export class VectorUtils implements IVectorUtils {
/**
* Creates an instance of VectorUtils
*/
protected getRouterChannelAddressSetup: ResultAsync<string, RouterUnavailableError | CoreUninitializedError> | null;
constructor(
protected configProvider: IConfigProvider,
protected contextProvider: IContextProvider,
protected browserNodeProvider: IBrowserNodeProvider,
protected blockchainProvider: IBlockchainProvider,
protected paymentIdUtils: IPaymentIdUtils,
protected logUtils: ILogUtils,
protected timeUtils: ITimeUtils,
) {
this.getRouterChannelAddressSetup = null;
}
/**
* Resolves a message/offer/null transfer with Vector.
* @param transferId the ID of the transfer to resolve
*/
public resolveMessageTransfer(transferId: string): ResultAsync<IBasicTransferResponse, TransferResolutionError> {
let channelAddress: string;
let browserNode: IBrowserNode;
return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this.getRouterChannelAddress()]).andThen(
() => {
return browserNode.resolveTransfer(channelAddress, transferId, { message: "" } as MessageResolver);
},
);
}
/**
* Resolves a parameterized payment transfer with Vector.
* @param transferId the ID of the transfer to resolve
*/
public resolvePaymentTransfer(
transferId: string,
paymentId: string,
amount: string,
): ResultAsync<IBasicTransferResponse, TransferResolutionError> {
const resolverData: ParameterizedResolverData = {
UUID: paymentId,
paymentAmountTaken: amount,
};
let channelAddress: string;
let browserNode: IBrowserNode;
return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this.getRouterChannelAddress()])
.andThen((vals) => {
const [browserNodeVal, channelAddressVal] = vals;
browserNode = browserNodeVal;
channelAddress = channelAddressVal;
const resolverDataEncoding = ["tuple(bytes32 UUID, uint256 paymentAmountTaken)"];
const encodedResolverData = defaultAbiCoder.encode(resolverDataEncoding, [resolverData]);
const hashedResolverData = keccak256(encodedResolverData);
return browserNode.signUtilityMessage(hashedResolverData);
})
.andThen((signature) => {
const resolver: ParameterizedResolver = {
data: resolverData,
payeeSignature: signature,
};
return browserNode.resolveTransfer(channelAddress, transferId, resolver);
});
}
/**
* Resolves an insurance transfer with Vector.
* @param transferId the ID of the tarnsfer to resolve
*/
public resolveInsuranceTransfer(
transferId: string,
paymentId: string,
mediatorSignature?: string,
amount?: BigNumber,
): ResultAsync<IBasicTransferResponse, TransferResolutionError> {
// If you do not provide an actual amount, then it resolves for nothing
if (amount == null) {
amount = BigNumber.from(0);
}
const resolverData: InsuranceResolverData = {
amount: amount.toString(),
UUID: paymentId,
};
let channelAddress: string;
let browserNode: IBrowserNode;
return ResultUtils.combine([this.browserNodeProvider.getBrowserNode(), this.getRouterChannelAddress()])
.andThen((vals) => {
const [browserNodeVal, channelAddressVal] = vals;
browserNode = browserNodeVal;
channelAddress = channelAddressVal;
if (mediatorSignature == null) {
const resolverDataEncoding = ["tuple(uint256 amount, bytes32 UUID)"];
const encodedResolverData = defaultAbiCoder.encode(resolverDataEncoding, [resolverData]);
const hashedResolverData = keccak256(encodedResolverData);
return browserNode.signUtilityMessage(hashedResolverData);
}
return okAsync<string, TransferResolutionError>(mediatorSignature);
})
.andThen((signature) => {
const resolver: InsuranceResolver = {
data: resolverData,
signature: signature,
};
return browserNode.resolveTransfer(channelAddress, transferId, resolver);
});
}
/**
* Creates a "Message" transfer with Vector, to notify the other party of a pull payment
* @param toAddress the public identifier (not eth address!) of the intended recipient
* @param message the message to send as IHypernetOfferDetails
*/
public createPullNotificationTransfer(
toAddress: string,
message: IHypernetPullPaymentDetails,
): ResultAsync<IBasicTransferResponse, TransferCreationError | InvalidParametersError> {
// The message type has to be PULLPAYMENT
message.messageType = EMessageTransferType.PULLPAYMENT;
// Sanity check - make sure the paymentId is valid:
const validPayment = this.paymentIdUtils.isValidPaymentId(message.paymentId);
if (validPayment.isErr()) {
return errAsync(validPayment.error);
} else {
if (!validPayment.value) {
return errAsync(
new InvalidParametersError(`CreatePullNotificationTransfer: Invalid paymentId: '${message.paymentId}'`),
);
}
}
const prerequisites = (ResultUtils.combine([
this.configProvider.getConfig() as ResultAsync<any, any>,
this.getRouterChannelAddress(),
this.browserNodeProvider.getBrowserNode(),
]) as unknown) as ResultAsync<
[HypernetConfig, string, IBrowserNode],
RouterChannelUnknownError | CoreUninitializedError | VectorError | Error
>;
return prerequisites.andThen((vals) => {
const [config, channelAddress, browserNode] = vals;
const initialState: MessageState = {
message: serialize(message),
};
return browserNode.conditionalTransfer(
channelAddress,
"0",
config.hypertokenAddress,
"MessageTransfer",
initialState,
toAddress,
undefined, // CRITICAL- must be undefined
undefined,
undefined,
message,
);
});
}
/**
* Creates a "Message" transfer with Vector, to notify the other party of a payment creation
* @param toAddress the public identifier (not eth address!) of the intended recipient
* @param message the message to send as IHypernetOfferDetails
*/
public createOfferTransfer(
toAddress: string,
message: IHypernetOfferDetails,
): ResultAsync<IBasicTransferResponse, TransferCreationError | InvalidParametersError> {
// The message type has to be OFFER
message.messageType = EMessageTransferType.OFFER;
// Sanity check - make sure the paymentId is valid:
const validPayment = this.paymentIdUtils.isValidPaymentId(message.paymentId);
if (validPayment.isErr()) {
return errAsync(validPayment.error);
} else {
if (!validPayment.value) {
return errAsync(new InvalidParametersError(`CreateOfferTransfer: Invalid paymentId: '${message.paymentId}'`));
}
}
return ResultUtils.combine([this.getRouterChannelAddress(), this.browserNodeProvider.getBrowserNode()]).andThen(
(vals) => {
const [channelAddress, browserNode] = vals;
const initialState: MessageState = {
message: serialize(message),
};
return browserNode.conditionalTransfer(
channelAddress,
"0",
message.paymentToken, // The offer is always for 0, so we will make the asset ID in the payment token type, because why not?
"MessageTransfer",
initialState,
toAddress,
undefined, // CRITICAL- must be undefined
undefined,
undefined,
message,
);
},
);
}
/**
* Creates a "Parameterized" transfer with Vector.
* @param type "PUSH" or "PULL"
* @param toAddress the public identifier of the intended recipient of this transfer
* @param amount the amount of tokens to commit to this transfer
* @param assetAddress the address of the ERC20-token to transfer; zero-address for ETH
* @param paymentId length-64 hexadecimal string; this becomes the UUID component of the InsuranceState
* @param start the start time of this transfer (UNIX timestamp)
* @param expiration the expiration time of this transfer (UNIX timestamp)
* @param rate the maximum allowed rate of this transfer (deltaAmount/deltaTime)
*/
public createPaymentTransfer(
type: EPaymentType,
toAddress: PublicIdentifier,
amount: BigNumber,
assetAddress: string,
paymentId: string,
start: number,
expiration: number,
deltaTime?: number,
deltaAmount?: string,
): ResultAsync<IBasicTransferResponse, TransferCreationError | InvalidParametersError> {
// Sanity check
if (type === EPaymentType.Pull && deltaTime === undefined) {
this.logUtils.error("Must provide deltaTime for Pull payments");
return errAsync(new InvalidParametersError("Must provide deltaTime for Pull payments"));
}
if (type === EPaymentType.Pull && deltaAmount === undefined) {
this.logUtils.error("Must provide deltaAmount for Pull payments");
return errAsync(new InvalidParametersError("Must provide deltaAmount for Pull payments"));
}
if (amount.isZero()) {
this.logUtils.error("Amount cannot be zero.");
return errAsync(new InvalidParametersError("Amount cannot be zero."));
}
// Make sure the paymentId is valid:
const validPayment = this.paymentIdUtils.isValidPaymentId(paymentId);
if (validPayment.isErr()) {
this.logUtils.error(validPayment.error);
return errAsync(validPayment.error);
} else {
if (!validPayment.value) {
this.logUtils.error(`CreatePaymentTransfer: Invalid paymentId: '${paymentId}'`);
return errAsync(new InvalidParametersError(`CreatePaymentTransfer: Invalid paymentId: '${paymentId}'`));
}
}
const prerequisites = (ResultUtils.combine([
this.getRouterChannelAddress() as ResultAsync<any, any>,
this.browserNodeProvider.getBrowserNode(),
]) as unknown) as ResultAsync<
[string, IBrowserNode],
RouterChannelUnknownError | CoreUninitializedError | VectorError | Error
>;
return prerequisites.andThen((vals) => {
const [channelAddress, browserNode] = vals;
const toEthAddress = getSignerAddressFromPublicIdentifier(toAddress);
// @todo toEthAddress isn't really an eth address, it's the internal signing key
// therefore we need to actually do the signing of the payment transfer (on resolve)
// with this internal key!
const infiniteRate = {
deltaAmount: amount.toString(),
deltaTime: "1",
};
let ourRate: Rate;
// Have to throw this error, or the ourRate object below will complain that one
// of the params is possibly undefined.
if (type == EPaymentType.Pull) {
if (deltaTime == null || deltaAmount == null) {
this.logUtils.error("Somehow, deltaTime or deltaAmount were not set!");
return errAsync(new InvalidParametersError("Somehow, deltaTime or deltaAmount were not set!"));
}
if (deltaTime == 0 || deltaAmount == "0") {
this.logUtils.error("deltatime & deltaAmount cannot be zero!");
return errAsync(new InvalidParametersError("deltatime & deltaAmount cannot be zero!"));
}
ourRate = {
deltaTime: deltaTime?.toString(),
deltaAmount: deltaAmount?.toString(),
};
} else {
ourRate = infiniteRate;
}
const initialState: ParameterizedState = {
receiver: toEthAddress,
start: start.toString(),
expiration: expiration.toString(),
UUID: paymentId,
rate: ourRate,
};
return browserNode.conditionalTransfer(
channelAddress,
amount.toString(),
assetAddress,
"Parameterized",
initialState,
toAddress,
undefined,
undefined,
undefined,
{}, // intentially left blank!
);
});
}
/**
* Creates the actual Insurance transfer with Vector
* @param toAddress the publicIdentifier of the person to send the transfer to
* @param mediatorAddress the Ethereum address of the mediator
* @param amount the amount of the token to commit into the InsuranceTransfer
* @param expiration the expiration date of this InsuranceTransfer
* @param paymentId a length-64 hexadecimal string; this becomes the UUID component of the InsuranceState
*/
public createInsuranceTransfer(
toAddress: PublicIdentifier,
mediatorPublicKey: PublicKey,
amount: BigNumber,
expiration: number,
paymentId: string,
): ResultAsync<IBasicTransferResponse, TransferCreationError | InvalidParametersError> {
// Sanity check - make sure the paymentId is valid:
const validPayment = this.paymentIdUtils.isValidPaymentId(paymentId);
if (validPayment.isErr()) {
return errAsync(validPayment.error);
} else {
if (!validPayment.value) {
return errAsync(new InvalidParametersError(`CreateInsuranceTransfer: Invalid paymentId: '${paymentId}'`));
}
}
const prerequisites = (ResultUtils.combine([
this.configProvider.getConfig() as ResultAsync<any, any>,
this.getRouterChannelAddress(),
this.browserNodeProvider.getBrowserNode(),
]) as unknown) as ResultAsync<
[HypernetConfig, string, IBrowserNode],
RouterChannelUnknownError | CoreUninitializedError | VectorError | Error
>;
return prerequisites.andThen((vals) => {
const [config, channelAddress, browserNode] = vals;
const toEthAddress = getSignerAddressFromPublicIdentifier(toAddress);
const initialState: InsuranceState = {
receiver: toEthAddress,
mediator: mediatorPublicKey,
collateral: amount.toString(),
expiration: expiration.toString(),
UUID: paymentId,
};
return browserNode.conditionalTransfer(
channelAddress,
amount.toString(),
config.hypertokenAddress,
"Insurance",
initialState,
toAddress,
undefined,
undefined,
undefined,
{}, // left intentionally blank!
);
});
}
/**
* Returns the address of the channel with the router, if exists.
* Otherwise, attempts to create a channel with the router & return the address.
*/
public getRouterChannelAddress(): ResultAsync<string, RouterChannelUnknownError | CoreUninitializedError> {
// If we already have the address, no need to do the rest
if (this.getRouterChannelAddressSetup != null) {
return this.getRouterChannelAddressSetup;
}
let config: HypernetConfig;
let context: InitializedHypernetContext;
let browserNode: IBrowserNode;
this.getRouterChannelAddressSetup = ResultUtils.combine([
this.configProvider.getConfig(),
this.contextProvider.getInitializedContext(),
this.browserNodeProvider.getBrowserNode(),
])
.andThen((vals) => {
[config, context, browserNode] = vals;
this.logUtils.log(`Core publicIdentifier: ${context.publicIdentifier}`);
this.logUtils.log(`Router publicIdentifier: ${config.routerPublicIdentifier}`);
return browserNode.getStateChannels();
})
.andThen((channelAddresses) => {
const channelResults = new Array<ResultAsync<IFullChannelState, RouterChannelUnknownError | VectorError>>();
for (const channelAddress of channelAddresses) {
channelResults.push(this._getStateChannel(channelAddress, browserNode));
}
return ResultUtils.combine(channelResults);
})
.andThen((channels) => {
for (const channel of channels) {
if (!channel) {
continue;
}
if (channel.aliceIdentifier !== config.routerPublicIdentifier) {
continue;
}
return okAsync(channel.channelAddress as string);
}
// If a channel does not exist with the router, we need to create it.
return this._createRouterStateChannel(browserNode, config).map((routerChannel) => {
return routerChannel.channelAddress;
});
});
return this.getRouterChannelAddressSetup;
}
public getTimestampFromTransfer(transfer: IFullTransferState): number {
if (transfer.meta == null) {
// We need to figure out the transfer type, I think; but for now we'll just say
// that the transfer is right now
return this.timeUtils.getUnixNow();
}
return transfer.meta.creationDate;
}
public getTransferStateFromTransfer(transfer: IFullTransferState): ETransferState {
// if (transfer.inDispute) {
// return ETransferState.Challenged;
// }
if (transfer.transferResolver != null) {
return ETransferState.Resolved;
}
return ETransferState.Active;
}
protected _createRouterStateChannel(
browserNode: IBrowserNode,
config: HypernetConfig,
): ResultAsync<IBasicChannelResponse, VectorError> {
return browserNode.setup(config.routerPublicIdentifier, config.chainId, DEFAULT_CHANNEL_TIMEOUT.toString());
}
protected _getStateChannel(
channelAddress: string,
browserNode: IBrowserNode,
): ResultAsync<IFullChannelState, RouterChannelUnknownError | VectorError> {
return browserNode.getStateChannel(channelAddress).andThen((channel) => {
if (channel == null) {
return errAsync(new RouterChannelUnknownError());
}
return okAsync(channel);
});
}
}