|
|
|
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";
|
|
|
|
|
|
|
|
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 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;
|
|
|
|
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) {
|
|
|
|
return did === HIDDEN_DID;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
|
|
|
**/
|
|
|
|
export function didInfo(did, identifiers, contacts) {
|
|
|
|
const myId = R.find((i) => i.did === did, identifiers);
|
|
|
|
if (myId) {
|
|
|
|
return "You";
|
|
|
|
} else {
|
|
|
|
const contact = R.find((c) => c.did === did, contacts);
|
|
|
|
if (contact) {
|
|
|
|
return contact.name || "Someone Unnamed in Contacts";
|
|
|
|
} else if (!did) {
|
|
|
|
return "Unpecified Person";
|
|
|
|
} else if (isHiddenDid(did)) {
|
|
|
|
return "Someone Not In Network";
|
|
|
|
} else {
|
|
|
|
return "Someone Not In Contacts";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For result, see https://endorser.ch:3000/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
|
|
|
|
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
|
|
|
// 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" };
|
|
|
|
}
|
|
|
|
// 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
|
|
|
|
if (identity.keys[0].privateKeyHex == null) {
|
|
|
|
return new Promise<InternalError>((resolve, reject) => {
|
|
|
|
reject({
|
|
|
|
error: "No private key",
|
|
|
|
message:
|
|
|
|
"Your identifier " +
|
|
|
|
identity.did +
|
|
|
|
" is not configured correctly. Use a different identifier.",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
|
|
|
return axios.post(url, payload, { headers });
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|