|
|
|
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 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<string>,
|
|
|
|
contacts: Array<Contact>,
|
|
|
|
): 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<ClaimResult>;
|
|
|
|
}
|
|
|
|
|
|
|
|
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<CreateAndSubmitGiveResult> {
|
|
|
|
try {
|
|
|
|
// Make a claim
|
|
|
|
const vcClaim: GiveVerifiableCredential = {
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
"@type": "GiveAction",
|
|
|
|
};
|
|
|
|
if (toDid) {
|
|
|
|
vcClaim.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;
|
|
|
|
}
|