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; } export interface GiverInputInfo { did?: string; name?: string; } 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; } 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 RegisterVerifiableCredential { "@context": string; "@type": string; agent: { identifier: string }; object: string; recipient: { 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: Array, contacts: Array, ): string { const myId: string | undefined = R.find(R.equals(did), allMyDids); if (myId) { return "You" + (myId !== activeDid ? " (Alt ID)" : ""); } else { const contact: Contact | undefined = R.find((c) => c.did === did, contacts); if (contact) { return contact.name || "Someone Unnamed in Contacts"; } else if (!did) { return "Unspecified Person"; } else if (isHiddenDid(did)) { return "Someone Not In Network"; } else { return "Someone Not In Contacts"; } } } interface SuccessResult { type: "success"; response: AxiosResponse; } interface ErrorResult { type: "error"; error: InternalError; } 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 { try { // Make a claim const vcClaim: GiveVerifiableCredential = { "@context": "https://schema.org", "@type": "GiveAction", recipient: { identifier: toDid }, }; if (fromDid) { vcClaim.agent = { identifier: fromDid }; } if (description) { vcClaim.description = description; } if (hours) { vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" }; } if (fulfillsProjectHandleId) { vcClaim.fulfills = { "@type": "PlanAction", identifier: fulfillsProjectHandleId, }; } // Make a payload for the claim 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]; if (!firstKey || !firstKey.privateKeyHex) { throw { error: "No private key", message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`, }; } 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); const alg = undefined; // Create a JWT for the request const vcJwt: string = await didJwt.createJWT(vcPayload, { alg: alg, issuer: identity.did, signer: signer, }); // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); const url = apiServer + "/api/v2/claim"; const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; const response = await axios.post(url, payload, { headers }); return { type: "success", response, }; } catch (error: unknown) { let errorMessage: string; if (error instanceof Error) { // If it's a JavaScript Error object errorMessage = error.message; } else if ( typeof error === "object" && error !== null && "message" in error ) { // If it's an object that has a 'message' property errorMessage = (error as { message: string }).message; } else { // Unknown error shape, default message errorMessage = "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; }