|
|
|
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 header line for contacts exported via Endorser Mobile
|
|
|
|
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
|
|
|
|
// 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<string, any>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GiverInputInfo {
|
|
|
|
did?: string;
|
|
|
|
name?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GiverOutputInfo {
|
|
|
|
action: string;
|
|
|
|
giver?: GiverInputInfo;
|
|
|
|
description?: string;
|
|
|
|
amount?: number;
|
|
|
|
unitCode?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ClaimResult {
|
|
|
|
success: { claimId: string; handleId: string };
|
|
|
|
error: { code: string; message: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GenericVerifiableCredential {
|
|
|
|
"@context": string;
|
|
|
|
"@type": string;
|
|
|
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GenericServerRecord extends GenericVerifiableCredential {
|
|
|
|
handleId?: string;
|
|
|
|
id: string;
|
|
|
|
issuedAt: string;
|
|
|
|
issuer: string;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
claim: Record<string, any>;
|
|
|
|
claimType?: string;
|
|
|
|
}
|
|
|
|
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
|
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "",
|
|
|
|
claim: {},
|
|
|
|
id: "",
|
|
|
|
issuedAt: "",
|
|
|
|
issuer: "",
|
|
|
|
};
|
|
|
|
|
|
|
|
// a summary record; the VC is found the fullClaim field
|
|
|
|
export interface GiveServerRecord {
|
|
|
|
agentDid: string;
|
|
|
|
amount: number;
|
|
|
|
amountConfirmed: number;
|
|
|
|
description: string;
|
|
|
|
fullClaim: GiveVerifiableCredential;
|
|
|
|
fulfillsPlanHandleId: string;
|
|
|
|
handleId: string;
|
|
|
|
issuedAt: string;
|
|
|
|
jwtId: string;
|
|
|
|
recipientDid: string;
|
|
|
|
unit: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// a summary record; the VC is found the fullClaim field
|
|
|
|
export interface OfferServerRecord {
|
|
|
|
amount: number;
|
|
|
|
amountGiven: number;
|
|
|
|
amountGivenConfirmed: number;
|
|
|
|
fullClaim: OfferVerifiableCredential;
|
|
|
|
fulfillsPlanHandleId: string;
|
|
|
|
handleId: string;
|
|
|
|
jwtId: string;
|
|
|
|
nonAmountGivenConfirmed: number;
|
|
|
|
objectDescription: string;
|
|
|
|
offeredByDid: string;
|
|
|
|
recipientDid: string;
|
|
|
|
requirementsMet: boolean;
|
|
|
|
unit: string;
|
|
|
|
validThrough: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// a summary record; the VC is not currently part of this record
|
|
|
|
export interface PlanServerRecord {
|
|
|
|
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
|
|
|
description: string;
|
|
|
|
endTime?: string;
|
|
|
|
fulfillsPlanHandleId: string;
|
|
|
|
handleId: string;
|
|
|
|
issuerDid: string;
|
|
|
|
locLat?: number;
|
|
|
|
locLon?: number;
|
|
|
|
startTime?: string;
|
|
|
|
url?: 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;
|
|
|
|
image?: 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;
|
|
|
|
agent?: { identifier: string };
|
|
|
|
description?: string;
|
|
|
|
identifier?: string;
|
|
|
|
lastClaimId?: string;
|
|
|
|
location?: {
|
|
|
|
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents data about a project
|
|
|
|
*
|
|
|
|
* @deprecated
|
|
|
|
* We should use PlanServerRecord instead.
|
|
|
|
**/
|
|
|
|
export interface PlanData {
|
|
|
|
/**
|
|
|
|
* Name of the project
|
|
|
|
**/
|
|
|
|
name: string;
|
|
|
|
/**
|
|
|
|
* Description of the project
|
|
|
|
**/
|
|
|
|
description: string;
|
|
|
|
/**
|
|
|
|
* URL referencing information about the project
|
|
|
|
**/
|
|
|
|
handleId: string;
|
|
|
|
/**
|
|
|
|
* The DID of the issuer
|
|
|
|
*/
|
|
|
|
issuerDid: string;
|
|
|
|
/**
|
|
|
|
* The Identier of the project -- different from jwtId, needs to be fixed
|
|
|
|
**/
|
|
|
|
rowid?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface EndorserRateLimits {
|
|
|
|
doneClaimsThisWeek: string;
|
|
|
|
doneRegistrationsThisMonth: string;
|
|
|
|
maxClaimsPerWeek: string;
|
|
|
|
maxRegistrationsPerMonth: string;
|
|
|
|
nextMonthBeginDateTime: string;
|
|
|
|
nextWeekBeginDateTime: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ImageRateLimits {
|
|
|
|
doneImagesThisWeek: string;
|
|
|
|
maxImagesPerWeek: string;
|
|
|
|
nextWeekBeginDateTime: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface VerifiableCredential {
|
|
|
|
"@context": string;
|
|
|
|
"@type": string;
|
|
|
|
name: string;
|
|
|
|
description: string;
|
|
|
|
identifier?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface WorldProperties {
|
|
|
|
startTime?: string;
|
|
|
|
endTime?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface RegisterVerifiableCredential {
|
|
|
|
"@context": string;
|
|
|
|
"@type": string;
|
|
|
|
agent: { identifier: string };
|
|
|
|
object: string;
|
|
|
|
participant: { identifier: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
// now for some of the error & other wrapper types
|
|
|
|
|
|
|
|
export interface ResultWithType {
|
|
|
|
type: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SuccessResult extends ResultWithType {
|
|
|
|
type: "success";
|
|
|
|
response: AxiosResponse<ClaimResult>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ErrorResponse {
|
|
|
|
error?: {
|
|
|
|
message?: string;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface InternalError {
|
|
|
|
error: string; // for system logging
|
|
|
|
userMessage?: string; // for user display
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ErrorResult extends ResultWithType {
|
|
|
|
type: "error";
|
|
|
|
error: InternalError;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
|
|
|
|
|
|
|
// 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 isDid(did: string) {
|
|
|
|
return did.startsWith("did:");
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isHiddenDid(did: string) {
|
|
|
|
return did === HIDDEN_DID;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isEmptyOrHiddenDid(did?: string) {
|
|
|
|
return !did || did === HIDDEN_DID; // catching empty string as well
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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 testRecursivelyOnStrings(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 (testRecursivelyOnStrings(func, input[key])) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// it's an array
|
|
|
|
for (const value of input) {
|
|
|
|
if (testRecursivelyOnStrings(func, value)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export function containsHiddenDid(obj: any) {
|
|
|
|
return testRecursivelyOnStrings(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 code is also contained 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")) {
|
|
|
|
result[key] = removeVisibleToDids(R.clone(input[key]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
} else {
|
|
|
|
// it's an array
|
|
|
|
return R.map(removeVisibleToDids, input);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function contactForDid(
|
|
|
|
did: string | undefined,
|
|
|
|
contacts: Contact[],
|
|
|
|
): Contact | undefined {
|
|
|
|
return isEmptyOrHiddenDid(did)
|
|
|
|
? undefined
|
|
|
|
: R.find((c) => c.did === did, contacts);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Similar logic is found in endorser-mobile.
|
|
|
|
*
|
|
|
|
* @param did
|
|
|
|
* @param activeDid
|
|
|
|
* @param contact
|
|
|
|
* @param allMyDids
|
|
|
|
* @return { known: boolean, displayName: string } where known is true if the display name is some easily-recogizable name, false if it's a generic name like "Someone Anonymous"
|
|
|
|
*/
|
|
|
|
export function didInfoForContact(
|
|
|
|
did: string | undefined,
|
|
|
|
activeDid: string | undefined,
|
|
|
|
contact?: Contact,
|
|
|
|
allMyDids: string[] = [],
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
): { known: boolean; displayName: string } {
|
|
|
|
if (!did) return { displayName: "Someone Anonymous", known: false };
|
|
|
|
if (contact) {
|
|
|
|
return {
|
|
|
|
displayName: contact.name || "Contact With No Name",
|
|
|
|
known: !!contact.name,
|
|
|
|
};
|
|
|
|
} else if (did === activeDid) {
|
|
|
|
return { displayName: "You", known: true };
|
|
|
|
} else {
|
|
|
|
const myId = R.find(R.equals(did), allMyDids);
|
|
|
|
return myId
|
|
|
|
? { displayName: "You (Alt ID)", known: true }
|
|
|
|
: isHiddenDid(did)
|
|
|
|
? { displayName: "Someone Outside Your Network", known: false }
|
|
|
|
: { displayName: "Someone Outside Contacts", known: false };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
always returns text, maybe something like "unnamed" or "unknown"
|
|
|
|
|
|
|
|
Now that we're using more informational didInfoForContact under the covers, we might want to consolidate.
|
|
|
|
**/
|
|
|
|
export function didInfo(
|
|
|
|
did: string | undefined,
|
|
|
|
activeDid: string | undefined,
|
|
|
|
allMyDids: string[],
|
|
|
|
contacts: Contact[],
|
|
|
|
): string {
|
|
|
|
const contact = contactForDid(did, contacts);
|
|
|
|
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 amount
|
|
|
|
* @param amount may be null; should have this or description
|
|
|
|
*/
|
|
|
|
export async function createAndSubmitGive(
|
|
|
|
axios: Axios,
|
|
|
|
apiServer: string,
|
|
|
|
identity: IIdentifier,
|
|
|
|
fromDid?: string | null,
|
|
|
|
toDid?: string,
|
|
|
|
description?: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
fulfillsOfferHandleId?: string,
|
|
|
|
isTrade: boolean = false,
|
|
|
|
imageUrl?: 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: amount
|
|
|
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
|
|
|
: undefined,
|
|
|
|
fulfills: [{ "@type": isTrade ? "TradeAction" : "DonateAction" }],
|
|
|
|
};
|
|
|
|
if (fulfillsProjectHandleId) {
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
|
|
|
|
vcClaim.fulfills.push({
|
|
|
|
"@type": "PlanAction",
|
|
|
|
identifier: fulfillsProjectHandleId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (fulfillsOfferHandleId) {
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
|
|
|
|
vcClaim.fulfills.push({
|
|
|
|
"@type": "Offer",
|
|
|
|
identifier: fulfillsOfferHandleId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (imageUrl) {
|
|
|
|
vcClaim.image = imageUrl;
|
|
|
|
}
|
|
|
|
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 amount
|
|
|
|
* @param amount 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,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
expirationDate?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
const vcClaim: OfferVerifiableCredential = {
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
"@type": "Offer",
|
|
|
|
offeredBy: { identifier: identity.did },
|
|
|
|
validThrough: expirationDate || undefined,
|
|
|
|
};
|
|
|
|
if (amount) {
|
|
|
|
vcClaim.includesObject = {
|
|
|
|
amountOfThisGood: amount,
|
|
|
|
unitCode: 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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// similar logic is found in endorser-mobile
|
|
|
|
export const createAndSubmitConfirmation = async (
|
|
|
|
identifier: IIdentifier,
|
|
|
|
claim: GenericVerifiableCredential,
|
|
|
|
lastClaimId: string, // used to set the lastClaimId
|
|
|
|
handleId: string | undefined,
|
|
|
|
apiServer: string,
|
|
|
|
axios: Axios,
|
|
|
|
) => {
|
|
|
|
const goodClaim = removeSchemaContext(
|
|
|
|
removeVisibleToDids(
|
|
|
|
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
const confirmationClaim: GenericVerifiableCredential = {
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
"@type": "AgreeAction",
|
|
|
|
object: goodClaim,
|
|
|
|
};
|
|
|
|
return createAndSubmitClaim(confirmationClaim, identifier, 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.error("Error creating claim:", error);
|
|
|
|
const errorMessage: string =
|
|
|
|
error.response?.data?.error?.message ||
|
|
|
|
error.message ||
|
|
|
|
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: "error",
|
|
|
|
error: {
|
|
|
|
error: errorMessage,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export const isAccept = (claim: Record<string, any>) => {
|
|
|
|
return (
|
|
|
|
claim &&
|
|
|
|
claim["@context"] === SCHEMA_ORG_CONTEXT &&
|
|
|
|
claim["@type"] === "AcceptAction"
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export const isOffer = (claim: Record<string, any>) => {
|
|
|
|
return (
|
|
|
|
claim &&
|
|
|
|
claim["@context"] === SCHEMA_ORG_CONTEXT &&
|
|
|
|
claim["@type"] === "Offer"
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export function currencyShortWordForCode(unitCode: string, single: boolean) {
|
|
|
|
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function displayAmount(code: string, amt: number) {
|
|
|
|
return "" + amt + " " + currencyShortWordForCode(code, amt === 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// insert a space before any capital letters except the initial letter
|
|
|
|
// (and capitalize initial letter, just in case)
|
|
|
|
export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
|
|
|
return !text
|
|
|
|
? ""
|
|
|
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
return readable summary of claim, or something generic
|
|
|
|
|
|
|
|
similar code is also contained in endorser-mobile
|
|
|
|
**/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
const claimSummary = (claim: Record<string, any>) => {
|
|
|
|
if (!claim) {
|
|
|
|
// to differentiate from "something" above
|
|
|
|
return "something";
|
|
|
|
}
|
|
|
|
if (claim.claim) {
|
|
|
|
// probably a Verified Credential
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
claim = claim.claim as Record<string, any>;
|
|
|
|
}
|
|
|
|
if (Array.isArray(claim)) {
|
|
|
|
if (claim.length === 1) {
|
|
|
|
claim = claim[0];
|
|
|
|
} else {
|
|
|
|
return "multiple claims";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const type = claim["@type"];
|
|
|
|
if (!type) {
|
|
|
|
return "a claim";
|
|
|
|
} else {
|
|
|
|
let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type);
|
|
|
|
if (typeExpl === "Person") {
|
|
|
|
typeExpl += " claim";
|
|
|
|
}
|
|
|
|
return "a " + typeExpl;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
return readable description of claim if possible, as a past-tense action
|
|
|
|
|
|
|
|
identifiers is a list of objects with a 'did' field, each representing the user
|
|
|
|
contacts is a list of objects with a 'did' field for others and a 'name' field for their name
|
|
|
|
|
|
|
|
similar code is also contained in endorser-mobile
|
|
|
|
**/
|
|
|
|
export const claimSpecialDescription = (
|
|
|
|
record: GenericServerRecord,
|
|
|
|
activeDid: string,
|
|
|
|
identifiers: Array<string>,
|
|
|
|
contacts: Array<Contact>,
|
|
|
|
) => {
|
|
|
|
let claim = record.claim;
|
|
|
|
if (claim.claim) {
|
|
|
|
// it's probably a Verified Credential
|
|
|
|
claim = claim.claim;
|
|
|
|
}
|
|
|
|
|
|
|
|
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
|
|
|
const type = claim["@type"] || "UnknownType";
|
|
|
|
|
|
|
|
if (type === "AgreeAction") {
|
|
|
|
return issuer + " agreed with " + claimSummary(claim.object);
|
|
|
|
} else if (isAccept(claim)) {
|
|
|
|
return issuer + " accepted " + claimSummary(claim.object);
|
|
|
|
} else if (type === "GiveAction") {
|
|
|
|
// agent.did is for legacy data, before March 2023
|
|
|
|
const giver = claim.agent?.identifier || claim.agent?.did;
|
|
|
|
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
|
|
|
let gaveAmount = claim.object?.amountOfThisGood
|
|
|
|
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
|
|
|
: "";
|
|
|
|
if (claim.description) {
|
|
|
|
if (gaveAmount) {
|
|
|
|
gaveAmount = gaveAmount + ", and also: ";
|
|
|
|
}
|
|
|
|
gaveAmount = gaveAmount + claim.description;
|
|
|
|
}
|
|
|
|
if (!gaveAmount) {
|
|
|
|
gaveAmount = "something not described";
|
|
|
|
}
|
|
|
|
// recipient.did is for legacy data, before March 2023
|
|
|
|
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
|
|
|
const gaveRecipientInfo = gaveRecipientId
|
|
|
|
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
|
|
|
: "";
|
|
|
|
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
|
|
|
} else if (type === "JoinAction") {
|
|
|
|
// agent.did is for legacy data, before March 2023
|
|
|
|
const agent = claim.agent?.identifier || claim.agent?.did;
|
|
|
|
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
|
|
|
|
|
|
|
let eventOrganizer =
|
|
|
|
claim.event && claim.event.organizer && claim.event.organizer.name;
|
|
|
|
eventOrganizer = eventOrganizer || "";
|
|
|
|
let eventName = claim.event && claim.event.name;
|
|
|
|
eventName = eventName ? " " + eventName : "";
|
|
|
|
let fullEvent = eventOrganizer + eventName;
|
|
|
|
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
|
|
|
|
|
|
|
let eventDate = claim.event && claim.event.startTime;
|
|
|
|
eventDate = eventDate ? " at " + eventDate : "";
|
|
|
|
return contactInfo + fullEvent + eventDate;
|
|
|
|
} else if (isOffer(claim)) {
|
|
|
|
const offerer = claim.offeredBy?.identifier;
|
|
|
|
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
|
|
|
let offering = "";
|
|
|
|
if (claim.includesObject) {
|
|
|
|
offering +=
|
|
|
|
" " +
|
|
|
|
displayAmount(
|
|
|
|
claim.includesObject.unitCode,
|
|
|
|
claim.includesObject.amountOfThisGood,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (claim.itemOffered?.description) {
|
|
|
|
offering += ", saying: " + claim.itemOffered?.description;
|
|
|
|
}
|
|
|
|
// recipient.did is for legacy data, before March 2023
|
|
|
|
const offerRecipientId =
|
|
|
|
claim.recipient?.identifier || claim.recipient?.did;
|
|
|
|
const offerRecipientInfo = offerRecipientId
|
|
|
|
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
|
|
|
|
: "";
|
|
|
|
return contactInfo + " offered" + offering + offerRecipientInfo;
|
|
|
|
} else if (type === "PlanAction") {
|
|
|
|
const claimer = claim.agent?.identifier || record.issuer;
|
|
|
|
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
|
|
|
return claimerInfo + " announced a project: " + claim.name;
|
|
|
|
} else if (type === "Tenure") {
|
|
|
|
// party.did is for legacy data, before March 2023
|
|
|
|
const claimer = claim.party?.identifier || claim.party?.did;
|
|
|
|
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
|
|
|
const polygon = claim.spatialUnit?.geo?.polygon || "";
|
|
|
|
return (
|
|
|
|
contactInfo +
|
|
|
|
" possesses [" +
|
|
|
|
polygon.substring(0, polygon.indexOf(" ")) +
|
|
|
|
"...]"
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return issuer + " declared " + claimSummary(claim as GenericServerRecord);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
|
|
|
|
process.env.VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID ||
|
|
|
|
"https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK"; // this won't resolve as a URL on production; it's a URN only found in the test system
|
|
|
|
|
|
|
|
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
|
|
|
|
return {
|
|
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "JoinAction",
|
|
|
|
agent: {
|
|
|
|
identifier: did,
|
|
|
|
},
|
|
|
|
event: {
|
|
|
|
organizer: {
|
|
|
|
name: "Bountiful Voluntaryist Community",
|
|
|
|
},
|
|
|
|
name: "Saturday Morning Meeting",
|
|
|
|
startTime: startTime,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|