You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
13 KiB
506 lines
13 KiB
import * as R from "ramda";
|
|
import { IIdentifier } from "@veramo/core";
|
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
|
import * as didJwt from "did-jwt";
|
|
import { Axios, AxiosResponse } from "axios";
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
|
// the object in RegisterAction claims
|
|
export const SERVICE_ID = "endorser.ch";
|
|
// the prefix for the contact URL
|
|
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
|
// the suffix for the contact URL
|
|
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
|
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
|
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
|
|
|
export interface AgreeVerifiableCredential {
|
|
"@context": string;
|
|
"@type": string;
|
|
// "any" because arbitrary objects can be subject of agreement
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
object: Record<any, any>;
|
|
}
|
|
|
|
export interface GiverInputInfo {
|
|
did?: string;
|
|
name?: string;
|
|
}
|
|
|
|
export interface GiverOutputInfo {
|
|
action: string;
|
|
giver?: GiverInputInfo;
|
|
description?: string;
|
|
hours?: number;
|
|
}
|
|
|
|
export interface ClaimResult {
|
|
success: { claimId: string; handleId: string };
|
|
error: { code: string; message: string };
|
|
}
|
|
|
|
export interface GenericVerifiableCredential {
|
|
"@context": string;
|
|
"@type": string;
|
|
}
|
|
|
|
export interface GenericServerRecord extends GenericVerifiableCredential {
|
|
handleId?: string;
|
|
id?: string;
|
|
issuedAt?: string;
|
|
issuer?: string;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
claim: Record<any, any>;
|
|
}
|
|
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
"@type": "",
|
|
claim: {},
|
|
};
|
|
|
|
export interface GiveServerRecord {
|
|
agentDid: string;
|
|
amount: number;
|
|
amountConfirmed: number;
|
|
description: string;
|
|
fullClaim: GiveVerifiableCredential;
|
|
handleId: string;
|
|
issuedAt: string;
|
|
recipientDid: string;
|
|
unit: string;
|
|
}
|
|
|
|
export interface OfferServerRecord {
|
|
amount: number;
|
|
amountGiven: number;
|
|
offeredByDid: string;
|
|
recipientDid: string;
|
|
requirementsMet: boolean;
|
|
unit: string;
|
|
validThrough: string;
|
|
}
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
// https://endorser.ch/doc/html/transactions.html#id4
|
|
export interface GiveVerifiableCredential {
|
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
|
"@type": "GiveAction";
|
|
agent?: { identifier: string };
|
|
description?: string;
|
|
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
|
|
identifier?: string;
|
|
object?: { amountOfThisGood: number; unitCode: string };
|
|
recipient?: { identifier: string };
|
|
}
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
// https://endorser.ch/doc/html/transactions.html#id8
|
|
export interface OfferVerifiableCredential {
|
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
|
"@type": "Offer";
|
|
description?: string;
|
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
|
itemOffered?: {
|
|
description?: string;
|
|
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
|
};
|
|
offeredBy?: { identifier: string };
|
|
validThrough?: string;
|
|
}
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
// https://endorser.ch/doc/html/transactions.html#id7
|
|
export interface PlanVerifiableCredential {
|
|
"@context": "https://schema.org";
|
|
"@type": "PlanAction";
|
|
name: string;
|
|
description: string;
|
|
identifier?: string;
|
|
location?: {
|
|
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
|
};
|
|
}
|
|
|
|
export interface PlanServerRecord {
|
|
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
|
description: string;
|
|
endTime?: string;
|
|
issuerDid: string;
|
|
handleId: string;
|
|
locLat?: number;
|
|
locLon?: number;
|
|
startTime?: string;
|
|
url?: string;
|
|
}
|
|
|
|
export interface RegisterVerifiableCredential {
|
|
"@context": string;
|
|
"@type": string;
|
|
agent: { identifier: string };
|
|
object: string;
|
|
participant: { identifier: string };
|
|
}
|
|
|
|
export interface InternalError {
|
|
error: string; // for system logging
|
|
userMessage?: string; // for user display
|
|
}
|
|
|
|
// This is used to check for hidden info.
|
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
|
const HIDDEN_DID = "did:none:HIDDEN";
|
|
|
|
export function isHiddenDid(did: string) {
|
|
return did === HIDDEN_DID;
|
|
}
|
|
|
|
/**
|
|
* @return true for any nested string where func(input) === true
|
|
*
|
|
* Similar logic is found in endorser-mobile.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
|
|
if (Object.prototype.toString.call(input) === "[object String]") {
|
|
return func(input);
|
|
} else if (input instanceof Object) {
|
|
if (!Array.isArray(input)) {
|
|
// it's an object
|
|
for (const key in input) {
|
|
if (testRecursivelyOnString(func, input[key])) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
// it's an array
|
|
for (const value of input) {
|
|
if (testRecursivelyOnString(func, value)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function containsHiddenDid(obj: any) {
|
|
return testRecursivelyOnString(isHiddenDid, obj);
|
|
}
|
|
|
|
export function stripEndorserPrefix(claimId: string) {
|
|
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
|
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
|
|
} else {
|
|
return claimId;
|
|
}
|
|
}
|
|
|
|
// similar logic is found in endorser-mobile
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function removeSchemaContext(obj: any) {
|
|
return obj["@context"] === SCHEMA_ORG_CONTEXT
|
|
? R.omit(["@context"], obj)
|
|
: obj;
|
|
}
|
|
|
|
// similar logic is found in endorser-mobile
|
|
export function addLastClaimOrHandleAsIdIfMissing(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
obj: any,
|
|
lastClaimId?: string,
|
|
handleId?: string,
|
|
) {
|
|
if (!obj.identifier && lastClaimId) {
|
|
const result = R.clone(obj);
|
|
result.lastClaimId = lastClaimId;
|
|
return result;
|
|
} else if (!obj.identifier && handleId) {
|
|
const result = R.clone(obj);
|
|
result.identifier = handleId;
|
|
return result;
|
|
} else {
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
// return clone of object without any nested *VisibleToDids keys
|
|
// similar logic is found in endorser-mobile
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function removeVisibleToDids(input: any): any {
|
|
if (input instanceof Object) {
|
|
if (!Array.isArray(input)) {
|
|
// it's an object
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const result: Record<string, any> = {};
|
|
for (const key in input) {
|
|
if (!key.endsWith("VisibleToDids")) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
result[key] = removeVisibleToDids(R.clone(input[key]));
|
|
}
|
|
}
|
|
return result;
|
|
} else {
|
|
// it's an array
|
|
return R.map(removeVisibleToDids, input);
|
|
}
|
|
return false;
|
|
} else {
|
|
return input;
|
|
}
|
|
}
|
|
|
|
/**
|
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
|
|
|
Similar logic is found in endorser-mobile.
|
|
**/
|
|
export function didInfo(
|
|
did: string,
|
|
activeDid: string,
|
|
allMyDids: string[],
|
|
contacts: Contact[],
|
|
): string {
|
|
if (!did) return "Someone Anonymous";
|
|
|
|
const myId = R.find(R.equals(did), allMyDids);
|
|
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
|
|
|
|
const contact = R.find((c) => c.did === did, contacts);
|
|
return contact
|
|
? contact.name || "Contact With No Name"
|
|
: isHiddenDid(did)
|
|
? "Someone Not In Network"
|
|
: "Someone Not In Contacts";
|
|
}
|
|
|
|
export interface ResultWithType {
|
|
type: string;
|
|
}
|
|
|
|
export interface SuccessResult extends ResultWithType {
|
|
type: "success";
|
|
response: AxiosResponse<ClaimResult>;
|
|
}
|
|
|
|
export interface ErrorResult {
|
|
type: "error";
|
|
error: InternalError;
|
|
}
|
|
|
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
|
|
|
/**
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
*
|
|
* @param identity
|
|
* @param fromDid may be null
|
|
* @param toDid
|
|
* @param description may be null; should have this or hours
|
|
* @param hours may be null; should have this or description
|
|
*/
|
|
export async function createAndSubmitGive(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
identity: IIdentifier,
|
|
fromDid?: string,
|
|
toDid?: string,
|
|
description?: string,
|
|
hours?: number,
|
|
unitCode?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim: GiveVerifiableCredential = {
|
|
"@context": "https://schema.org",
|
|
"@type": "GiveAction",
|
|
recipient: toDid ? { identifier: toDid } : undefined,
|
|
agent: fromDid ? { identifier: fromDid } : undefined,
|
|
description: description || undefined,
|
|
object: hours
|
|
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
|
|
: undefined,
|
|
fulfills: fulfillsProjectHandleId
|
|
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
|
: undefined,
|
|
};
|
|
return createAndSubmitClaim(
|
|
vcClaim as GenericServerRecord,
|
|
identity,
|
|
apiServer,
|
|
axios,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
*
|
|
* @param identity
|
|
* @param description may be null; should have this or hours
|
|
* @param hours may be null; should have this or description
|
|
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
|
*/
|
|
export async function createAndSubmitOffer(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
identity: IIdentifier,
|
|
description?: string,
|
|
hours?: number,
|
|
expirationDate?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim: OfferVerifiableCredential = {
|
|
"@context": "https://schema.org",
|
|
"@type": "Offer",
|
|
offeredBy: { identifier: identity.did },
|
|
validThrough: expirationDate || undefined,
|
|
};
|
|
if (hours) {
|
|
vcClaim.includesObject = {
|
|
amountOfThisGood: hours,
|
|
unitCode: "HUR",
|
|
};
|
|
}
|
|
if (description) {
|
|
vcClaim.itemOffered = { description };
|
|
}
|
|
if (fulfillsProjectHandleId) {
|
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
|
vcClaim.itemOffered.isPartOf = {
|
|
"@type": "PlanAction",
|
|
identifier: fulfillsProjectHandleId,
|
|
};
|
|
}
|
|
return createAndSubmitClaim(
|
|
vcClaim as GenericServerRecord,
|
|
identity,
|
|
apiServer,
|
|
axios,
|
|
);
|
|
}
|
|
|
|
export async function createAndSubmitClaim(
|
|
vcClaim: GenericVerifiableCredential,
|
|
identity: IIdentifier,
|
|
apiServer: string,
|
|
axios: Axios,
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
try {
|
|
const vcPayload = {
|
|
vc: {
|
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
type: ["VerifiableCredential"],
|
|
credentialSubject: vcClaim,
|
|
},
|
|
};
|
|
|
|
// Create a signature using private key of identity
|
|
const firstKey = identity.keys[0];
|
|
const privateKeyHex = firstKey?.privateKeyHex;
|
|
|
|
if (!privateKeyHex) {
|
|
throw {
|
|
error: "No private key",
|
|
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
|
};
|
|
}
|
|
|
|
const signer = await SimpleSigner(privateKeyHex);
|
|
|
|
// Create a JWT for the request
|
|
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
|
issuer: identity.did,
|
|
signer,
|
|
});
|
|
|
|
// Make the xhr request payload
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
|
const url = `${apiServer}/api/v2/claim`;
|
|
const token = await accessToken(identity);
|
|
|
|
const response = await axios.post(url, payload, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
return { type: "success", response };
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
console.log("Error creating claim:", error);
|
|
const errorMessage: string =
|
|
error.response?.data?.error?.message || error.message || "Unknown error";
|
|
|
|
return {
|
|
type: "error",
|
|
error: {
|
|
error: errorMessage,
|
|
userMessage: "Failed to create and submit the claim.",
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// from https://stackoverflow.com/a/175787/845494
|
|
//
|
|
export function isNumeric(str: string): boolean {
|
|
return !isNaN(+str);
|
|
}
|
|
|
|
export function numberOrZero(str: string): number {
|
|
return isNumeric(str) ? +str : 0;
|
|
}
|
|
|
|
export interface ErrorResponse {
|
|
error?: {
|
|
message?: string;
|
|
};
|
|
}
|
|
|
|
export interface RateLimits {
|
|
doneClaimsThisWeek: string;
|
|
doneRegistrationsThisMonth: string;
|
|
maxClaimsPerWeek: string;
|
|
maxRegistrationsPerMonth: string;
|
|
nextMonthBeginDateTime: string;
|
|
nextWeekBeginDateTime: string;
|
|
}
|
|
|
|
/**
|
|
* Represents data about a project
|
|
**/
|
|
export interface ProjectData {
|
|
/**
|
|
* Name of the project
|
|
**/
|
|
name: string;
|
|
/**
|
|
* Description of the project
|
|
**/
|
|
description: string;
|
|
/**
|
|
* URL referencing information about the project
|
|
**/
|
|
handleId: string;
|
|
/**
|
|
* The Identier of the project
|
|
**/
|
|
rowid: string;
|
|
}
|
|
|
|
export interface VerifiableCredential {
|
|
"@context": string;
|
|
"@type": string;
|
|
name: string;
|
|
description: string;
|
|
identifier?: string;
|
|
}
|
|
|
|
export interface WorldProperties {
|
|
startTime?: string;
|
|
endTime?: string;
|
|
}
|
|
|