|
|
|
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
|
|
|
import { Buffer } from "buffer";
|
|
|
|
import { sha256 } from "ethereum-cryptography/sha256";
|
|
|
|
import { LRUCache } from "lru-cache";
|
|
|
|
import * as R from "ramda";
|
|
|
|
|
|
|
|
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
|
|
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto";
|
|
|
|
import { NonsensitiveDexie } from "@/db/index";
|
|
|
|
import {
|
|
|
|
getAccount,
|
|
|
|
getPasskeyExpirationSeconds,
|
|
|
|
GiverReceiverInputInfo,
|
|
|
|
} from "@/libs/util";
|
|
|
|
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
|
|
|
|
import { Account } from "@/db/tables/accounts";
|
|
|
|
|
|
|
|
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 GiverOutputInfo {
|
|
|
|
action: string;
|
|
|
|
giver?: GiverReceiverInputInfo;
|
|
|
|
description?: string;
|
|
|
|
amount?: number;
|
|
|
|
unitCode?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ClaimResult {
|
|
|
|
success: { claimId: string; handleId: string };
|
|
|
|
error: { code: string; message: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
// similar to VerifiableCredentialSubject... maybe rename this
|
|
|
|
export interface GenericVerifiableCredential {
|
|
|
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
|
|
|
"@type": string;
|
|
|
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
|
|
|
claim: T;
|
|
|
|
claimType?: string;
|
|
|
|
handleId: string;
|
|
|
|
id: string;
|
|
|
|
issuedAt: string;
|
|
|
|
issuer: string;
|
|
|
|
publicUrls?: Record<string, string>; // only for IDs that want to be public
|
|
|
|
}
|
|
|
|
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
|
|
|
{
|
|
|
|
claim: { "@type": "" },
|
|
|
|
handleId: "",
|
|
|
|
id: "",
|
|
|
|
issuedAt: "",
|
|
|
|
issuer: "",
|
|
|
|
};
|
|
|
|
|
|
|
|
// a summary record; the VC is found the fullClaim field
|
|
|
|
export interface GiveSummaryRecord {
|
|
|
|
agentDid: string;
|
|
|
|
amount: number;
|
|
|
|
amountConfirmed: number;
|
|
|
|
description: string;
|
|
|
|
fullClaim: GiveVerifiableCredential;
|
|
|
|
fulfillsHandleId: string;
|
|
|
|
fulfillsPlanHandleId?: string;
|
|
|
|
fulfillsType?: string;
|
|
|
|
handleId: string;
|
|
|
|
issuedAt: string;
|
|
|
|
issuerDid: string;
|
|
|
|
jwtId: string;
|
|
|
|
providerPlanHandleId?: string;
|
|
|
|
recipientDid: string;
|
|
|
|
unit: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// a summary record; the VC is found the fullClaim field
|
|
|
|
export interface OfferSummaryRecord {
|
|
|
|
amount: number;
|
|
|
|
amountGiven: number;
|
|
|
|
amountGivenConfirmed: number;
|
|
|
|
fullClaim: OfferVerifiableCredential;
|
|
|
|
fulfillsPlanHandleId: string;
|
|
|
|
handleId: string;
|
|
|
|
issuerDid: 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 PlanSummaryRecord {
|
|
|
|
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
|
|
|
description: string;
|
|
|
|
endTime?: string;
|
|
|
|
fulfillsPlanHandleId: string;
|
|
|
|
handleId: string;
|
|
|
|
image?: string;
|
|
|
|
issuerDid: string;
|
|
|
|
locLat?: number;
|
|
|
|
locLon?: number;
|
|
|
|
name?: string;
|
|
|
|
startTime?: string;
|
|
|
|
url?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
|
|
// https://endorser.ch/doc/html/transactions.html#id4
|
|
|
|
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
|
|
|
"@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 };
|
|
|
|
provider?: GenericVerifiableCredential; // typically @type & identifier
|
|
|
|
recipient?: { identifier: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
|
|
// https://endorser.ch/doc/html/transactions.html#id8
|
|
|
|
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
|
|
|
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
|
|
|
|
"@type": "Offer";
|
|
|
|
description?: string; // conditions for the offer
|
|
|
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
|
|
|
itemOffered?: {
|
|
|
|
description?: string; // description of the item
|
|
|
|
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
|
|
|
};
|
|
|
|
offeredBy?: { identifier: string };
|
|
|
|
recipient?: { identifier: string };
|
|
|
|
validThrough?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note that previous VCs may have additional fields.
|
|
|
|
// https://endorser.ch/doc/html/transactions.html#id7
|
|
|
|
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
|
|
|
"@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 PlanSummaryRecord instead.
|
|
|
|
**/
|
|
|
|
export interface PlanData {
|
|
|
|
/**
|
|
|
|
* Name of the project
|
|
|
|
**/
|
|
|
|
name: string;
|
|
|
|
/**
|
|
|
|
* Description of the project
|
|
|
|
**/
|
|
|
|
description: string;
|
|
|
|
/**
|
|
|
|
* URL referencing information about the project
|
|
|
|
**/
|
|
|
|
handleId: string;
|
|
|
|
image?: string;
|
|
|
|
/**
|
|
|
|
* The DID of the issuer
|
|
|
|
*/
|
|
|
|
issuerDid: string;
|
|
|
|
/**
|
|
|
|
* The identifier 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 {
|
|
|
|
exp?: number;
|
|
|
|
iat: number;
|
|
|
|
iss: string;
|
|
|
|
vc: {
|
|
|
|
"@context": string[];
|
|
|
|
type: string[];
|
|
|
|
credentialSubject: VerifiableCredentialSubject;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// similar to GenericVerifiableCredential... maybe replace that one
|
|
|
|
export interface VerifiableCredentialSubject {
|
|
|
|
"@context": string;
|
|
|
|
"@type": string;
|
|
|
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface WorldProperties {
|
|
|
|
startTime?: string;
|
|
|
|
endTime?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// AKA Registration & RegisterAction
|
|
|
|
export interface RegisterVerifiableCredential {
|
|
|
|
"@context": typeof SCHEMA_ORG_CONTEXT;
|
|
|
|
"@type": "RegisterAction";
|
|
|
|
agent: { identifier: string };
|
|
|
|
identifier?: string; // used for invites (when participant ID isn't known)
|
|
|
|
object: string;
|
|
|
|
participant?: { identifier: string }; // used when person is known (not an invite)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
export interface UserInfo {
|
|
|
|
name: string;
|
|
|
|
publicEncKey: string;
|
|
|
|
registered: boolean;
|
|
|
|
profileImageUrl?: string;
|
|
|
|
nextPublicEncKeyHash?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 string within this primitive/object/array 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export const containsNonHiddenDid = (obj: any) => {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), 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, profileImageUrl?: string }
|
|
|
|
* where 'known' is true if they are in the contacts
|
|
|
|
*/
|
|
|
|
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; profileImageUrl?: string } {
|
|
|
|
if (!did) return { displayName: "Someone Unnamed/Unknown", known: false };
|
|
|
|
if (did === activeDid) {
|
|
|
|
return { displayName: "You", known: true };
|
|
|
|
} else if (contact) {
|
|
|
|
return {
|
|
|
|
displayName: contact.name || "Contact With No Name",
|
|
|
|
known: !!contact,
|
|
|
|
profileImageUrl: contact.profileImageUrl,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const myId = R.find(R.equals(did), allMyDids);
|
|
|
|
return myId
|
|
|
|
? { displayName: "You (Alt ID)", known: true }
|
|
|
|
: isHiddenDid(did)
|
|
|
|
? { displayName: "Someone Totally Outside Your View", known: false }
|
|
|
|
: {
|
|
|
|
displayName: "Someone Visible But Outside Your Contact List",
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
let passkeyAccessToken: string = "";
|
|
|
|
let passkeyTokenExpirationEpochSeconds: number = 0;
|
|
|
|
|
|
|
|
export function clearPasskeyToken() {
|
|
|
|
passkeyAccessToken = "";
|
|
|
|
passkeyTokenExpirationEpochSeconds = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function tokenExpiryTimeDescription() {
|
|
|
|
if (
|
|
|
|
!passkeyAccessToken ||
|
|
|
|
passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000
|
|
|
|
) {
|
|
|
|
return "Token has expired";
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
"Token expires at " +
|
|
|
|
new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the headers for a request, potentially including Authorization
|
|
|
|
*/
|
|
|
|
export async function getHeaders(did?: string) {
|
|
|
|
const headers: { "Content-Type": string; Authorization?: string } = {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
};
|
|
|
|
if (did) {
|
|
|
|
let token;
|
|
|
|
const account = await getAccount(did);
|
|
|
|
if (account?.passkeyCredIdHex) {
|
|
|
|
if (
|
|
|
|
passkeyAccessToken &&
|
|
|
|
passkeyTokenExpirationEpochSeconds > Date.now() / 1000
|
|
|
|
) {
|
|
|
|
// there's an active current passkey token
|
|
|
|
token = passkeyAccessToken;
|
|
|
|
} else {
|
|
|
|
// there's no current passkey token or it's expired
|
|
|
|
token = await accessToken(did);
|
|
|
|
|
|
|
|
passkeyAccessToken = token;
|
|
|
|
const passkeyExpirationSeconds = await getPasskeyExpirationSeconds();
|
|
|
|
passkeyTokenExpirationEpochSeconds =
|
|
|
|
Date.now() / 1000 + passkeyExpirationSeconds;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
token = await accessToken(did);
|
|
|
|
}
|
|
|
|
headers["Authorization"] = "Bearer " + token;
|
|
|
|
} else {
|
|
|
|
// it's often OK to request without auth; we assume necessary checks are done earlier
|
|
|
|
}
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
|
|
|
max: 500,
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param handleId nullable, in which case "undefined" will be returned
|
|
|
|
* @param requesterDid optional, in which case no private info will be returned
|
|
|
|
* @param axios
|
|
|
|
* @param apiServer
|
|
|
|
*/
|
|
|
|
export async function getPlanFromCache(
|
|
|
|
handleId: string | undefined,
|
|
|
|
axios: Axios,
|
|
|
|
apiServer: string,
|
|
|
|
requesterDid?: string,
|
|
|
|
): Promise<PlanSummaryRecord | undefined> {
|
|
|
|
if (!handleId) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
let cred = planCache.get(handleId);
|
|
|
|
if (!cred) {
|
|
|
|
const url =
|
|
|
|
apiServer +
|
|
|
|
"/api/v2/report/plans?handleId=" +
|
|
|
|
encodeURIComponent(handleId);
|
|
|
|
const headers = await getHeaders(requesterDid);
|
|
|
|
try {
|
|
|
|
const resp = await axios.get(url, { headers });
|
|
|
|
if (resp.status === 200 && resp.data?.data?.length > 0) {
|
|
|
|
cred = resp.data.data[0];
|
|
|
|
planCache.set(handleId, cred);
|
|
|
|
} else {
|
|
|
|
console.error(
|
|
|
|
"Failed to load plan with handle",
|
|
|
|
handleId,
|
|
|
|
" Got data:",
|
|
|
|
resp.data,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(
|
|
|
|
"Failed to load plan with handle",
|
|
|
|
handleId,
|
|
|
|
" Got error:",
|
|
|
|
error,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cred;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function setPlanInCache(
|
|
|
|
handleId: string,
|
|
|
|
planSummary: PlanSummaryRecord,
|
|
|
|
) {
|
|
|
|
planCache.set(handleId, planSummary);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct GiveAction VC for submission to server
|
|
|
|
*
|
|
|
|
* @param lastClaimId supplied when editing a previous claim
|
|
|
|
*/
|
|
|
|
export function hydrateGive(
|
|
|
|
vcClaimOrig?: GiveVerifiableCredential,
|
|
|
|
fromDid?: string,
|
|
|
|
toDid?: string,
|
|
|
|
description?: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
fulfillsOfferHandleId?: string,
|
|
|
|
isTrade: boolean = false,
|
|
|
|
imageUrl?: string,
|
|
|
|
providerPlanHandleId?: string,
|
|
|
|
lastClaimId?: string,
|
|
|
|
): GiveVerifiableCredential {
|
|
|
|
// Remember: replace values or erase if it's null
|
|
|
|
|
|
|
|
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
|
|
|
? R.clone(vcClaimOrig)
|
|
|
|
: {
|
|
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "GiveAction",
|
|
|
|
};
|
|
|
|
|
|
|
|
if (lastClaimId) {
|
|
|
|
// this is an edit
|
|
|
|
vcClaim.lastClaimId = lastClaimId;
|
|
|
|
delete vcClaim.identifier;
|
|
|
|
}
|
|
|
|
|
|
|
|
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
|
|
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
|
|
|
vcClaim.description = description || undefined;
|
|
|
|
vcClaim.object =
|
|
|
|
amount && !isNaN(amount)
|
|
|
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
// ensure fulfills is an array
|
|
|
|
if (!Array.isArray(vcClaim.fulfills)) {
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
|
|
|
}
|
|
|
|
// ... and replace or add each element, ending with Trade or Donate
|
|
|
|
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
|
|
(elem) => elem["@type"] !== "PlanAction",
|
|
|
|
);
|
|
|
|
if (fulfillsProjectHandleId) {
|
|
|
|
vcClaim.fulfills.push({
|
|
|
|
"@type": "PlanAction",
|
|
|
|
identifier: fulfillsProjectHandleId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
|
|
(elem) => elem["@type"] !== "Offer",
|
|
|
|
);
|
|
|
|
if (fulfillsOfferHandleId) {
|
|
|
|
vcClaim.fulfills.push({
|
|
|
|
"@type": "Offer",
|
|
|
|
identifier: fulfillsOfferHandleId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// do Trade/Donate last because current endorser.ch only looks at the first for plans & offers
|
|
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
|
|
(elem) =>
|
|
|
|
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
|
|
|
|
);
|
|
|
|
vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" });
|
|
|
|
|
|
|
|
vcClaim.image = imageUrl || undefined;
|
|
|
|
|
|
|
|
vcClaim.provider = providerPlanHandleId
|
|
|
|
? { "@type": "PlanAction", identifier: providerPlanHandleId }
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
return vcClaim;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
|
|
*
|
|
|
|
* @param fromDid may be null
|
|
|
|
* @param toDid
|
|
|
|
* @param description may be null
|
|
|
|
* @param amount may be null
|
|
|
|
*/
|
|
|
|
export async function createAndSubmitGive(
|
|
|
|
axios: Axios,
|
|
|
|
apiServer: string,
|
|
|
|
issuerDid: string,
|
|
|
|
fromDid?: string,
|
|
|
|
toDid?: string,
|
|
|
|
description?: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
fulfillsOfferHandleId?: string,
|
|
|
|
isTrade: boolean = false,
|
|
|
|
imageUrl?: string,
|
|
|
|
providerPlanHandleId?: string,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
const vcClaim = hydrateGive(
|
|
|
|
undefined,
|
|
|
|
fromDid,
|
|
|
|
toDid,
|
|
|
|
description,
|
|
|
|
amount,
|
|
|
|
unitCode,
|
|
|
|
fulfillsProjectHandleId,
|
|
|
|
fulfillsOfferHandleId,
|
|
|
|
isTrade,
|
|
|
|
imageUrl,
|
|
|
|
providerPlanHandleId,
|
|
|
|
undefined,
|
|
|
|
);
|
|
|
|
return createAndSubmitClaim(
|
|
|
|
vcClaim as GenericVerifiableCredential,
|
|
|
|
issuerDid,
|
|
|
|
apiServer,
|
|
|
|
axios,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
|
|
*
|
|
|
|
* @param fromDid may be null
|
|
|
|
* @param toDid may be null if project is provided
|
|
|
|
* @param description may be null
|
|
|
|
* @param amount may be null
|
|
|
|
*/
|
|
|
|
export async function editAndSubmitGive(
|
|
|
|
axios: Axios,
|
|
|
|
apiServer: string,
|
|
|
|
fullClaim: GenericCredWrapper<GiveVerifiableCredential>,
|
|
|
|
issuerDid: string,
|
|
|
|
fromDid?: string,
|
|
|
|
toDid?: string,
|
|
|
|
description?: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
fulfillsOfferHandleId?: string,
|
|
|
|
isTrade: boolean = false,
|
|
|
|
imageUrl?: string,
|
|
|
|
providerPlanHandleId?: string,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
const vcClaim = hydrateGive(
|
|
|
|
fullClaim.claim,
|
|
|
|
fromDid,
|
|
|
|
toDid,
|
|
|
|
description,
|
|
|
|
amount,
|
|
|
|
unitCode,
|
|
|
|
fulfillsProjectHandleId,
|
|
|
|
fulfillsOfferHandleId,
|
|
|
|
isTrade,
|
|
|
|
imageUrl,
|
|
|
|
providerPlanHandleId,
|
|
|
|
fullClaim.id,
|
|
|
|
);
|
|
|
|
return createAndSubmitClaim(
|
|
|
|
vcClaim as GenericVerifiableCredential,
|
|
|
|
issuerDid,
|
|
|
|
apiServer,
|
|
|
|
axios,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct Offer VC for submission to server
|
|
|
|
*
|
|
|
|
* @param lastClaimId supplied when editing a previous claim
|
|
|
|
*/
|
|
|
|
export function hydrateOffer(
|
|
|
|
vcClaimOrig?: OfferVerifiableCredential,
|
|
|
|
fromDid?: string,
|
|
|
|
toDid?: string,
|
|
|
|
itemDescription?: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
conditionDescription?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
validThrough?: string,
|
|
|
|
lastClaimId?: string,
|
|
|
|
): OfferVerifiableCredential {
|
|
|
|
// Remember: replace values or erase if it's null
|
|
|
|
|
|
|
|
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
|
|
|
? R.clone(vcClaimOrig)
|
|
|
|
: {
|
|
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "Offer",
|
|
|
|
};
|
|
|
|
|
|
|
|
if (lastClaimId) {
|
|
|
|
// this is an edit
|
|
|
|
vcClaim.lastClaimId = lastClaimId;
|
|
|
|
delete vcClaim.identifier;
|
|
|
|
}
|
|
|
|
|
|
|
|
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
|
|
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
|
|
|
vcClaim.description = conditionDescription || undefined;
|
|
|
|
|
|
|
|
vcClaim.includesObject =
|
|
|
|
amount && !isNaN(amount)
|
|
|
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
if (itemDescription || fulfillsProjectHandleId) {
|
|
|
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
|
|
|
vcClaim.itemOffered.description = itemDescription || undefined;
|
|
|
|
if (fulfillsProjectHandleId) {
|
|
|
|
vcClaim.itemOffered.isPartOf = {
|
|
|
|
"@type": "PlanAction",
|
|
|
|
identifier: fulfillsProjectHandleId,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
vcClaim.validThrough = validThrough || undefined;
|
|
|
|
|
|
|
|
return vcClaim;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
|
|
*
|
|
|
|
* @param identity
|
|
|
|
* @param description may be null
|
|
|
|
* @param amount may be null
|
|
|
|
* @param validThrough 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,
|
|
|
|
issuerDid: string,
|
|
|
|
itemDescription: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
conditionDescription?: string,
|
|
|
|
validThrough?: string,
|
|
|
|
recipientDid?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
const vcClaim = hydrateOffer(
|
|
|
|
undefined,
|
|
|
|
issuerDid,
|
|
|
|
recipientDid,
|
|
|
|
itemDescription,
|
|
|
|
amount,
|
|
|
|
unitCode,
|
|
|
|
conditionDescription,
|
|
|
|
fulfillsProjectHandleId,
|
|
|
|
validThrough,
|
|
|
|
undefined,
|
|
|
|
);
|
|
|
|
return createAndSubmitClaim(
|
|
|
|
vcClaim as OfferVerifiableCredential,
|
|
|
|
issuerDid,
|
|
|
|
apiServer,
|
|
|
|
axios,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function editAndSubmitOffer(
|
|
|
|
axios: Axios,
|
|
|
|
apiServer: string,
|
|
|
|
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
|
|
|
issuerDid: string,
|
|
|
|
itemDescription: string,
|
|
|
|
amount?: number,
|
|
|
|
unitCode?: string,
|
|
|
|
conditionDescription?: string,
|
|
|
|
validThrough?: string,
|
|
|
|
recipientDid?: string,
|
|
|
|
fulfillsProjectHandleId?: string,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
const vcClaim = hydrateOffer(
|
|
|
|
fullClaim.claim,
|
|
|
|
issuerDid,
|
|
|
|
recipientDid,
|
|
|
|
itemDescription,
|
|
|
|
amount,
|
|
|
|
unitCode,
|
|
|
|
conditionDescription,
|
|
|
|
fulfillsProjectHandleId,
|
|
|
|
validThrough,
|
|
|
|
fullClaim.id,
|
|
|
|
);
|
|
|
|
return createAndSubmitClaim(
|
|
|
|
vcClaim as OfferVerifiableCredential,
|
|
|
|
issuerDid,
|
|
|
|
apiServer,
|
|
|
|
axios,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// similar logic is found in endorser-mobile
|
|
|
|
export const createAndSubmitConfirmation = async (
|
|
|
|
issuerDid: string,
|
|
|
|
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": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "AgreeAction",
|
|
|
|
object: goodClaim,
|
|
|
|
};
|
|
|
|
return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios);
|
|
|
|
};
|
|
|
|
|
|
|
|
export async function createAndSubmitClaim(
|
|
|
|
vcClaim: GenericVerifiableCredential,
|
|
|
|
issuerDid: string,
|
|
|
|
apiServer: string,
|
|
|
|
axios: Axios,
|
|
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
|
|
try {
|
|
|
|
const vcPayload = {
|
|
|
|
vc: {
|
|
|
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
|
|
type: ["VerifiableCredential"],
|
|
|
|
credentialSubject: vcClaim,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
|
|
|
|
|
|
|
|
// Make the xhr request payload
|
|
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
|
|
|
const url = `${apiServer}/api/v2/claim`;
|
|
|
|
|
|
|
|
const response = await axios.post(url, payload, {
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return { type: "success", response };
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error("Error submitting 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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function generateEndorserJwtForAccount(
|
|
|
|
account: Account,
|
|
|
|
isRegistered?: boolean,
|
|
|
|
name?: string,
|
|
|
|
profileImageUrl?: string,
|
|
|
|
// note that including the next key pushes QR codes to the next resolution smaller
|
|
|
|
includeNextKeyIfDerived?: boolean,
|
|
|
|
) {
|
|
|
|
const publicKeyHex = account.publicKeyHex;
|
|
|
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
|
|
|
|
|
|
|
const contactInfo = {
|
|
|
|
iat: Date.now(),
|
|
|
|
iss: account.did,
|
|
|
|
own: {
|
|
|
|
name: name ?? "",
|
|
|
|
publicEncKey,
|
|
|
|
registered: !!isRegistered,
|
|
|
|
} as UserInfo,
|
|
|
|
};
|
|
|
|
if (profileImageUrl) {
|
|
|
|
contactInfo.own.profileImageUrl = profileImageUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (includeNextKeyIfDerived && account?.mnemonic && account?.derivationPath) {
|
|
|
|
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
|
|
|
const nextPublicHex = deriveAddress(
|
|
|
|
account.mnemonic as string,
|
|
|
|
newDerivPath,
|
|
|
|
)[2];
|
|
|
|
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
|
|
|
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
|
|
|
const nextPublicEncKeyHashBase64 =
|
|
|
|
Buffer.from(nextPublicEncKeyHash).toString("base64");
|
|
|
|
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
|
|
|
|
}
|
|
|
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
|
|
|
|
|
|
|
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
|
|
|
return viewPrefix + vcJwt;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createEndorserJwtForDid(
|
|
|
|
issuerDid: string,
|
|
|
|
payload: object,
|
|
|
|
expiresIn?: number,
|
|
|
|
) {
|
|
|
|
const account = await getAccount(issuerDid);
|
|
|
|
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An AcceptAction is when someone accepts some contract or pledge.
|
|
|
|
*
|
|
|
|
* @param claim has properties '@context' & '@type'
|
|
|
|
* @return true if the claim is a schema.org AcceptAction
|
|
|
|
*/
|
|
|
|
// 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: GenericCredWrapper<GenericVerifiableCredential>,
|
|
|
|
) => {
|
|
|
|
if (!claim) {
|
|
|
|
// to differentiate from "something" above
|
|
|
|
return "something";
|
|
|
|
}
|
|
|
|
let specificClaim:
|
|
|
|
| GenericVerifiableCredential
|
|
|
|
| GenericCredWrapper<GenericVerifiableCredential> = claim;
|
|
|
|
if (claim.claim) {
|
|
|
|
// probably a Verified Credential
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
specificClaim = claim.claim;
|
|
|
|
}
|
|
|
|
if (Array.isArray(specificClaim)) {
|
|
|
|
if (specificClaim.length === 1) {
|
|
|
|
specificClaim = specificClaim[0];
|
|
|
|
} else {
|
|
|
|
return "multiple claims";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const type = specificClaim["@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: GenericCredWrapper<GenericVerifiableCredential>,
|
|
|
|
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 GenericCredWrapper<GenericVerifiableCredential>)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
|
|
|
|
import.meta.env.VITE_BVC_MEETUPS_PROJECT_CLAIM_ID ||
|
|
|
|
"https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H"; // production value, which seems like the safest value if forgotten
|
|
|
|
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export async function createEndorserJwtVcFromClaim(
|
|
|
|
issuerDid: string,
|
|
|
|
claim: object,
|
|
|
|
) {
|
|
|
|
// Make a payload for the claim
|
|
|
|
const vcPayload = {
|
|
|
|
vc: {
|
|
|
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
|
|
type: ["VerifiableCredential"],
|
|
|
|
credentialSubject: claim,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return createEndorserJwtForDid(issuerDid, vcPayload);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createInviteJwt(
|
|
|
|
activeDid: string,
|
|
|
|
contact?: Contact,
|
|
|
|
inviteId?: string,
|
|
|
|
expiresIn?: number,
|
|
|
|
): Promise<string> {
|
|
|
|
const vcClaim: RegisterVerifiableCredential = {
|
|
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
|
|
"@type": "RegisterAction",
|
|
|
|
agent: { identifier: activeDid },
|
|
|
|
object: SERVICE_ID,
|
|
|
|
};
|
|
|
|
if (contact) {
|
|
|
|
vcClaim.participant = { identifier: contact.did };
|
|
|
|
}
|
|
|
|
if (inviteId) {
|
|
|
|
vcClaim.identifier = inviteId;
|
|
|
|
}
|
|
|
|
// 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 vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
|
|
|
return vcJwt;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function register(
|
|
|
|
activeDid: string,
|
|
|
|
apiServer: string,
|
|
|
|
axios: Axios,
|
|
|
|
contact: Contact,
|
|
|
|
): Promise<{ success?: boolean; error?: string }> {
|
|
|
|
const vcJwt = await createInviteJwt(activeDid, contact);
|
|
|
|
|
|
|
|
const url = apiServer + "/api/v2/claim";
|
|
|
|
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
|
|
|
if (resp.data?.success?.handleId) {
|
|
|
|
return { success: true };
|
|
|
|
} else if (resp.data?.success?.embeddedRecordError) {
|
|
|
|
let message =
|
|
|
|
"There was some problem with the registration and so it may not be complete.";
|
|
|
|
if (typeof resp.data.success.embeddedRecordError == "string") {
|
|
|
|
message += " " + resp.data.success.embeddedRecordError;
|
|
|
|
}
|
|
|
|
return { error: message };
|
|
|
|
} else {
|
|
|
|
console.error(resp);
|
|
|
|
return { error: "Got a server error when registering." };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function setVisibilityUtil(
|
|
|
|
activeDid: string,
|
|
|
|
apiServer: string,
|
|
|
|
axios: Axios,
|
|
|
|
db: NonsensitiveDexie,
|
|
|
|
contact: Contact,
|
|
|
|
visibility: boolean,
|
|
|
|
) {
|
|
|
|
if (!activeDid) {
|
|
|
|
return { error: "Cannot set visibility without an identifier." };
|
|
|
|
}
|
|
|
|
const url =
|
|
|
|
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
|
|
|
|
const headers = await getHeaders(activeDid);
|
|
|
|
const payload = JSON.stringify({ did: contact.did });
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await axios.post(url, payload, { headers });
|
|
|
|
if (resp.status === 200) {
|
|
|
|
const success = resp.data.success;
|
|
|
|
if (success) {
|
|
|
|
db.contacts.update(contact.did, { seesMe: visibility });
|
|
|
|
}
|
|
|
|
return { success };
|
|
|
|
} else {
|
|
|
|
console.error(
|
|
|
|
"Got some bad server response when setting visibility: ",
|
|
|
|
resp.status,
|
|
|
|
resp,
|
|
|
|
);
|
|
|
|
const message =
|
|
|
|
resp.data.error?.message || "Got some error setting visibility.";
|
|
|
|
return { error: message };
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Got some error when setting visibility:", err);
|
|
|
|
return { error: "Check connectivity and try again." };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches rate limits from the Endorser server.
|
|
|
|
*
|
|
|
|
* @param apiServer endorser server URL string
|
|
|
|
* @param axios Axios instance
|
|
|
|
* @param {string} issuerDid - The DID for which to check rate limits.
|
|
|
|
* @returns {Promise<AxiosResponse>} The Axios response object.
|
|
|
|
*/
|
|
|
|
export async function fetchEndorserRateLimits(
|
|
|
|
apiServer: string,
|
|
|
|
axios: Axios,
|
|
|
|
issuerDid: string,
|
|
|
|
) {
|
|
|
|
const url = `${apiServer}/api/report/rateLimits`;
|
|
|
|
const headers = await getHeaders(issuerDid);
|
|
|
|
return await axios.get(url, { headers } as AxiosRequestConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches rate limits from the image server.
|
|
|
|
*
|
|
|
|
* @param apiServer image server URL string
|
|
|
|
* @param axios Axios instance
|
|
|
|
* @param {string} issuerDid - The DID for which to check rate limits.
|
|
|
|
* @returns {Promise<AxiosResponse>} The Axios response object.
|
|
|
|
*/
|
|
|
|
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
|
|
|
|
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
|
|
|
|
const headers = await getHeaders(issuerDid);
|
|
|
|
return await axios.get(url, { headers } as AxiosRequestConfig);
|
|
|
|
}
|