@test-org122/hypernet-core
Version:
Hypernet Core. Represents the SDK for running the Hypernet Protocol.
309 lines (268 loc) • 10.7 kB
text/typescript
import { IMerchantConnectorRepository } from "@interfaces/data";
import { PublicKey, BigNumber, HypernetContext, HypernetConfig } from "@interfaces/objects";
import {
CoreUninitializedError,
MerchantConnectorError,
MerchantValidationError,
PersistenceError,
} from "@interfaces/objects/errors";
import { errAsync, okAsync, ResultAsync } from "neverthrow";
import { ResultUtils, IAjaxUtils } from "@test-org122/utils";
import {
IBlockchainProvider,
IBlockchainUtils,
IConfigProvider,
IContextProvider,
ILocalStorageUtils,
IMerchantConnectorProxy,
IVectorUtils,
} from "@interfaces/utilities";
import { ethers } from "ethers";
import { TypedDataDomain, TypedDataField } from "@ethersproject/abstract-signer";
import { IMerchantConnectorProxyFactory } from "@interfaces/utilities/factory";
interface IAuthorizedMerchantEntry {
merchantUrl: string;
authorizationSignature: string;
}
export class MerchantConnectorRepository implements IMerchantConnectorRepository {
protected activatedMerchants: Map<string, IMerchantConnectorProxy>;
protected domain: TypedDataDomain;
protected types: Record<string, TypedDataField[]>;
constructor(
protected blockchainProvider: IBlockchainProvider,
protected ajaxUtils: IAjaxUtils,
protected configProvider: IConfigProvider,
protected contextProvider: IContextProvider,
protected vectorUtils: IVectorUtils,
protected localStorageUtils: ILocalStorageUtils,
protected merchantConnectorProxyFactory: IMerchantConnectorProxyFactory,
protected blockchainUtils: IBlockchainUtils,
) {
this.activatedMerchants = new Map();
this.domain = {
name: "Hypernet Protocol",
version: "1",
};
this.types = {
AuthorizedMerchant: [
{ name: "authorizedMerchantUrl", type: "string" },
{ name: "merchantValidatedSignature", type: "string" },
],
};
}
public getMerchantPublicKeys(merchantUrls: string[]): ResultAsync<Map<string, PublicKey>, Error> {
// TODO: right now, the merchant will publish a URL with their public key; eventually, they should be held in a smart contract
// For merchants that are already authorized, we can just go to their connector for the
// public key.
const publicKeyRequests = new Array<
ResultAsync<{ merchantUrl: string; publicKey: PublicKey }, MerchantConnectorError | Error>
>();
for (const merchantUrl of merchantUrls) {
const merchantProxy = this.activatedMerchants.get(merchantUrl);
if (merchantProxy != null) {
publicKeyRequests.push(
merchantProxy.getPublicKey().map((publicKey) => {
return { merchantUrl, publicKey };
}),
);
} else {
// Need to get it from the source
const url = new URL(merchantUrl.toString());
url.pathname = "publicKey";
publicKeyRequests.push(
this.ajaxUtils.get<string, Error>(url).map((publicKey) => {
return { merchantUrl, publicKey };
}),
);
}
}
return ResultUtils.combine(publicKeyRequests).map((vals) => {
const returnMap = new Map<string, PublicKey>();
for (const val of vals) {
returnMap.set(val.merchantUrl.toString(), val.publicKey);
}
return returnMap;
});
}
public addAuthorizedMerchant(merchantUrl: string): ResultAsync<void, PersistenceError> {
let proxy: IMerchantConnectorProxy;
let context: HypernetContext;
// First, we will create the proxy
return this.contextProvider
.getContext()
.andThen((myContext) => {
context = myContext;
return this.merchantConnectorProxyFactory.factoryProxy(merchantUrl);
})
.andThen((myProxy) => {
proxy = myProxy;
// With the proxy activated, we can get the validated merchant signature
return ResultUtils.combine([proxy.getValidatedSignature(), this.blockchainProvider.getSigner()]);
})
.andThen((vals) => {
const [merchantSignature, signer] = vals;
// merchantSignature has been validated by the iframe, so this is already confirmed.
// Now we need to get an authorization signature
const value = {
authorizedMerchantUrl: merchantUrl,
merchantValidatedSignature: merchantSignature,
} as Record<string, any>;
const signerPromise = signer._signTypedData(this.domain, this.types, value);
return ResultAsync.fromPromise(signerPromise, (e) => e as MerchantValidationError);
})
.andThen((authorizationSignature) => {
const authorizedMerchants = this._getAuthorizedMerchants();
authorizedMerchants.set(merchantUrl, authorizationSignature);
this._setAuthorizedMerchants(authorizedMerchants);
// Activate the merchant connector
return proxy.activateConnector();
})
.map(() => {
// Only if the merchant is successfully activated do we stick it in the list.
this.activatedMerchants.set(merchantUrl, proxy);
})
.mapErr((e) => {
// If we encounter a problem, destroy the proxy so we can start afresh.
if (proxy != null) {
proxy.destroy();
}
// Notify the world
if (context != null) {
context.onAuthorizedMerchantActivationFailed.next(merchantUrl);
}
return e;
});
}
/**
* Returns a map of merchant URLs with their authorization signatures.
*/
public getAuthorizedMerchants(): ResultAsync<Map<string, string>, PersistenceError> {
const authorizedMerchants = this._getAuthorizedMerchants();
return okAsync(authorizedMerchants);
}
public resolveChallenge(
merchantUrl: string,
paymentId: string,
transferId: string,
): ResultAsync<void, MerchantConnectorError | MerchantValidationError | CoreUninitializedError> {
const proxy = this.activatedMerchants.get(merchantUrl);
if (proxy == null) {
return errAsync(new MerchantValidationError(`No existing merchant connector for ${merchantUrl}`));
}
return proxy
.resolveChallenge(paymentId)
.andThen((result) => {
const { mediatorSignature, amount } = result;
return this.vectorUtils.resolveInsuranceTransfer(
transferId,
paymentId,
mediatorSignature,
BigNumber.from(amount),
);
})
.map(() => {});
}
protected _setAuthorizedMerchants(authorizedMerchantMap: Map<string, string>) {
const authorizedMerchantEntries = new Array<IAuthorizedMerchantEntry>();
for (const keyval of authorizedMerchantMap) {
authorizedMerchantEntries.push({ merchantUrl: keyval[0], authorizationSignature: keyval[1] });
}
this.localStorageUtils.setItem("AuthorizedMerchants", JSON.stringify(authorizedMerchantEntries));
}
protected _getAuthorizedMerchants(): Map<string, string> {
let authorizedMerchantStr = this.localStorageUtils.getItem("AuthorizedMerchants");
if (authorizedMerchantStr == null) {
authorizedMerchantStr = "[]";
}
const authorizedMerchantEntries = JSON.parse(authorizedMerchantStr) as IAuthorizedMerchantEntry[];
const authorizedMerchants = new Map<string, string>();
for (const authorizedMerchantEntry of authorizedMerchantEntries) {
authorizedMerchants.set(authorizedMerchantEntry.merchantUrl, authorizedMerchantEntry.authorizationSignature);
}
return authorizedMerchants;
}
public activateAuthorizedMerchants(): ResultAsync<
void,
MerchantConnectorError | MerchantValidationError | CoreUninitializedError
> {
return ResultUtils.combine([
this.contextProvider.getInitializedContext(),
this.getAuthorizedMerchants(),
this.blockchainProvider.getSigner(),
]).andThen((vals) => {
const [context, authorizedMerchants, signer] = vals;
const activationResults = new Array<ResultAsync<void, Error>>();
for (const keyval of authorizedMerchants) {
activationResults.push(
this._activateAuthorizedMerchant(
context.account,
keyval[0], // URL
keyval[1],
context,
signer,
),
);
}
return ResultUtils.combine(activationResults).map(() => {});
});
}
protected _activateAuthorizedMerchant(
accountAddress: string,
merchantUrl: string,
authorizationSignature: string,
context: HypernetContext,
signer: ethers.providers.JsonRpcSigner,
): ResultAsync<void, MerchantConnectorError | MerchantValidationError> {
let proxy: IMerchantConnectorProxy;
return this.merchantConnectorProxyFactory
.factoryProxy(merchantUrl)
.andThen((myProxy) => {
proxy = myProxy;
// We need to get the validated signature, so we can see if it was authorized
return proxy.getValidatedSignature();
})
.andThen((validatedSignature) => {
const value = {
authorizedMerchantUrl: merchantUrl,
merchantValidatedSignature: validatedSignature,
} as Record<string, any>;
const validationAddress = this.blockchainUtils.verifyTypedData(
this.domain,
this.types,
value,
authorizationSignature,
);
if (validationAddress !== accountAddress) {
// Notify the user that one of their authorized merchants has changed their code
context.onAuthorizedMerchantUpdated.next(merchantUrl);
// Get a new signature
// validatedSignature means the code is signed by the provider, so we just need
// to sign this new version.
const signerPromise = signer._signTypedData(this.domain, this.types, value);
return ResultAsync.fromPromise(signerPromise, (e) => e as MerchantConnectorError).map(
(newAuthorizationSignature) => {
const authorizedMerchants = this._getAuthorizedMerchants();
authorizedMerchants.set(merchantUrl.toString(), newAuthorizationSignature);
this._setAuthorizedMerchants(authorizedMerchants);
},
);
}
return okAsync<void, MerchantValidationError>(undefined);
})
.andThen(() => {
return proxy.activateConnector();
})
.map(() => {
this.activatedMerchants.set(merchantUrl, proxy);
})
.mapErr((e) => {
// The connector could not be authenticated, so just get rid of it.
if (proxy != null) {
proxy.destroy();
}
// Notify the world
context.onAuthorizedMerchantActivationFailed.next(merchantUrl);
return e;
});
}
}