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.
		
		
		
		
		
			
		
			
				
					
					
						
							298 lines
						
					
					
						
							7.3 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							298 lines
						
					
					
						
							7.3 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";
							 | 
						|
								export const SERVICE_ID = "endorser.ch";
							 | 
						|
								
							 | 
						|
								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 GenericClaim {
							 | 
						|
								  "@context": string;
							 | 
						|
								  "@type": string;
							 | 
						|
								  issuedAt: string;
							 | 
						|
								  // "any" because arbitrary objects can be subject of agreement
							 | 
						|
								  // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								  claim: Record<any, any>;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export interface GiveServerRecord {
							 | 
						|
								  agentDid: string;
							 | 
						|
								  amount: number;
							 | 
						|
								  amountConfirmed: number;
							 | 
						|
								  description: string;
							 | 
						|
								  fullClaim: GiveVerifiableCredential;
							 | 
						|
								  handleId: string;
							 | 
						|
								  issuedAt: string;
							 | 
						|
								  recipientDid: string;
							 | 
						|
								  unit: string;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export interface GiveVerifiableCredential {
							 | 
						|
								  "@context"?: string; // optional when embedded, eg. in an Agree
							 | 
						|
								  "@type": string;
							 | 
						|
								  agent?: { identifier: string };
							 | 
						|
								  description?: string;
							 | 
						|
								  fulfills?: { "@type": string; identifier: string };
							 | 
						|
								  identifier?: string;
							 | 
						|
								  object?: { amountOfThisGood: number; unitCode: string };
							 | 
						|
								  recipient?: { identifier: string };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								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;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
							 | 
						|
								 **/
							 | 
						|
								export function didInfo(
							 | 
						|
								  did: string,
							 | 
						|
								  activeDid: string,
							 | 
						|
								  allMyDids: string[],
							 | 
						|
								  contacts: Contact[],
							 | 
						|
								): string {
							 | 
						|
								  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 || "Someone Unnamed in Contacts"
							 | 
						|
								    : !did
							 | 
						|
								    ? "Unspecified Person"
							 | 
						|
								    : 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 CreateAndSubmitGiveResult = 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,
							 | 
						|
								  fulfillsProjectHandleId?: string,
							 | 
						|
								): Promise<CreateAndSubmitGiveResult> {
							 | 
						|
								  try {
							 | 
						|
								    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: "HUR" } : undefined,
							 | 
						|
								      fulfills: fulfillsProjectHandleId
							 | 
						|
								        ? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
							 | 
						|
								        : undefined,
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    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 };
							 | 
						|
								  } catch (error: unknown) {
							 | 
						|
								    const errorMessage: string =
							 | 
						|
								      error instanceof Error
							 | 
						|
								        ? error.message
							 | 
						|
								        : typeof error === "object" && "message" in error
							 | 
						|
								        ? (error as { message: string }).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;
							 | 
						|
								}
							 | 
						|
								
							 |