|
@ -69,18 +69,29 @@ |
|
|
|
|
|
|
|
|
<!-- eslint-disable-next-line --> |
|
|
<!-- eslint-disable-next-line --> |
|
|
<div |
|
|
<div |
|
|
class="block flex w-full" |
|
|
class="flex justify-between" |
|
|
> |
|
|
> |
|
|
<!-- eslint-disable-next-line --> |
|
|
<!-- eslint-disable-next-line --> |
|
|
|
|
|
<div class="w-1/2 text-left"> |
|
|
<button |
|
|
<button |
|
|
href="" |
|
|
href="" |
|
|
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" |
|
|
class="left text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" |
|
|
v-if="showGiveTotals" |
|
|
v-if="showGiveTotals" |
|
|
@click="loadGives()" |
|
|
@click="loadGives()" |
|
|
> |
|
|
> |
|
|
Load Totals |
|
|
Load Totals |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="w-1/2 text-right"> |
|
|
|
|
|
Hours to Add: |
|
|
|
|
|
<input |
|
|
|
|
|
class="border border rounded border-slate-400 w-24 text-right" |
|
|
|
|
|
type="text" |
|
|
|
|
|
placeholder="1" |
|
|
|
|
|
v-model="hourInput" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- Results List --> |
|
|
<!-- Results List --> |
|
|
<ul class=""> |
|
|
<ul class=""> |
|
@ -97,11 +108,20 @@ |
|
|
<div class="text-sm truncate">{{ contact.publicKeyBase64 }}</div> |
|
|
<div class="text-sm truncate">{{ contact.publicKeyBase64 }}</div> |
|
|
<div v-if="showGiveTotals" class="float-right"> |
|
|
<div v-if="showGiveTotals" class="float-right"> |
|
|
<div class="float-right"> |
|
|
<div class="float-right"> |
|
|
to: {{ contactGivenTotals[contact.did] || 0 }} |
|
|
to: {{ givenByMeTotals[contact.did] || 0 }} |
|
|
</div> |
|
|
<button |
|
|
<br /> |
|
|
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" |
|
|
<div class="float-right"> |
|
|
@click="onClickAddGive(identity.did, contact.did)" |
|
|
from: {{ contactReceivedTotals[contact.did] || 0 }} |
|
|
> |
|
|
|
|
|
+ |
|
|
|
|
|
</button> |
|
|
|
|
|
by: {{ givenToMeTotals[contact.did] || 0 }} |
|
|
|
|
|
<button |
|
|
|
|
|
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" |
|
|
|
|
|
@click="onClickAddGive(contact.did, identity.did)" |
|
|
|
|
|
> |
|
|
|
|
|
+ |
|
|
|
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
@ -111,14 +131,24 @@ |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
<script lang="ts"> |
|
|
|
|
|
import { AxiosError } from "axios"; |
|
|
|
|
|
import * as didJwt from "did-jwt"; |
|
|
import { Options, Vue } from "vue-class-component"; |
|
|
import { Options, Vue } from "vue-class-component"; |
|
|
import { IIdentifier } from "@veramo/core"; |
|
|
|
|
|
|
|
|
|
|
|
import { AppString } from "@/constants/app"; |
|
|
import { AppString } from "@/constants/app"; |
|
|
import { accessToken } from "@/libs/crypto"; |
|
|
import { accessToken, SimpleSigner } from "@/libs/crypto"; |
|
|
|
|
|
import { IIdentifier } from "@veramo/core"; |
|
|
import { db } from "../db"; |
|
|
import { db } from "../db"; |
|
|
import { Contact } from "../db/tables/contacts.ts"; |
|
|
import { Contact } from "../db/tables/contacts.ts"; |
|
|
|
|
|
|
|
|
|
|
|
export interface GiveVerifiableCredential { |
|
|
|
|
|
"@context": string; |
|
|
|
|
|
"@type": string; |
|
|
|
|
|
agent: { identifier: string }; |
|
|
|
|
|
recipient: { identifier: string }; |
|
|
|
|
|
object: { amountOfThisGood: number; unitCode: string }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
@Options({ |
|
|
@Options({ |
|
|
components: {}, |
|
|
components: {}, |
|
|
}) |
|
|
}) |
|
@ -126,9 +156,10 @@ export default class ContactsView extends Vue { |
|
|
contacts: Contact[] = []; |
|
|
contacts: Contact[] = []; |
|
|
contactInput = ""; |
|
|
contactInput = ""; |
|
|
// { "did:...": amount } entry for each contact |
|
|
// { "did:...": amount } entry for each contact |
|
|
contactGivenTotals = {}; |
|
|
givenByMeTotals = {}; |
|
|
// { "did:...": amount } entry for each contact |
|
|
// { "did:...": amount } entry for each contact |
|
|
contactReceivedTotals = {}; |
|
|
givenToMeTotals = {}; |
|
|
|
|
|
hourInput = 0; |
|
|
identity: IIdentifier = null; |
|
|
identity: IIdentifier = null; |
|
|
errorMessage = ""; |
|
|
errorMessage = ""; |
|
|
showGiveTotals = false; |
|
|
showGiveTotals = false; |
|
@ -188,7 +219,7 @@ export default class ContactsView extends Vue { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
//console.log("Done retrieving gives", contactTotals); |
|
|
//console.log("Done retrieving gives", contactTotals); |
|
|
this.contactGivenTotals = contactTotals; |
|
|
this.givenByMeTotals = contactTotals; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
this.errorMessage = "" + error; |
|
|
this.errorMessage = "" + error; |
|
@ -216,11 +247,117 @@ export default class ContactsView extends Vue { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
//console.log("Done retrieving receipts", contactTotals); |
|
|
//console.log("Done retrieving receipts", contactTotals); |
|
|
this.contactReceivedTotals = contactTotals; |
|
|
this.givenToMeTotals = contactTotals; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
this.errorMessage = "" + error; |
|
|
this.errorMessage = "" + error; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// from https://stackoverflow.com/a/175787/845494 |
|
|
|
|
|
private isNumeric(str): boolean { |
|
|
|
|
|
return !isNaN(str) && !isNaN(parseFloat(str)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async onClickAddGive(fromDid: string, toDid: string): void { |
|
|
|
|
|
if (!this.hourInput) { |
|
|
|
|
|
this.errorMessage = "Giving 0 hours does nothing."; |
|
|
|
|
|
} else if (!this.isNumeric(this.hourInput)) { |
|
|
|
|
|
this.errorMessage = |
|
|
|
|
|
"This is not a valid number of hours: " + this.hourInput; |
|
|
|
|
|
} else { |
|
|
|
|
|
this.errorMessage = ""; |
|
|
|
|
|
this.createAndSubmitGive(fromDid, toDid, parseFloat(this.hourInput)); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async createAndSubmitGive( |
|
|
|
|
|
fromDid: string, |
|
|
|
|
|
toDid: string, |
|
|
|
|
|
amount: number |
|
|
|
|
|
): void { |
|
|
|
|
|
// Make a claim |
|
|
|
|
|
const vcClaim: GiveVerifiableCredential = { |
|
|
|
|
|
"@context": "https://schema.org", |
|
|
|
|
|
"@type": "GiveAction", |
|
|
|
|
|
agent: { identifier: fromDid }, |
|
|
|
|
|
recipient: { identifier: toDid }, |
|
|
|
|
|
object: { amountOfThisGood: amount, 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 (this.identity.keys[0].privateKeyHex !== null) { |
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
|
|
|
|
|
const privateKeyHex: string = this.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: this.identity.did, |
|
|
|
|
|
signer: signer, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Make the xhr request payload |
|
|
|
|
|
|
|
|
|
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt }); |
|
|
|
|
|
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER; |
|
|
|
|
|
const url = endorserApiServer + "/api/v2/claim"; |
|
|
|
|
|
const token = await accessToken(this.identity); |
|
|
|
|
|
const headers = { |
|
|
|
|
|
"Content-Type": "application/json", |
|
|
|
|
|
Authorization: "Bearer " + token, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const resp = await this.axios.post(url, payload, { headers }); |
|
|
|
|
|
console.log("Got resp data:", resp.data); |
|
|
|
|
|
if (resp.data?.success?.handleId) { |
|
|
|
|
|
this.errorMessage = ""; |
|
|
|
|
|
this.alertTitle = ""; |
|
|
|
|
|
this.alertMessage = ""; |
|
|
|
|
|
if (fromDid === this.identity.did) { |
|
|
|
|
|
this.givenByMeTotals[toDid] = this.givenByMeTotals[toDid] + amount; |
|
|
|
|
|
// do this to update the UI |
|
|
|
|
|
// eslint-disable-next-line no-self-assign |
|
|
|
|
|
this.givenByMeTotals = this.givenByMeTotals; |
|
|
|
|
|
} else { |
|
|
|
|
|
this.givenToMeTotals[fromDid] = |
|
|
|
|
|
this.givenToMeTotals[fromDid] + amount; |
|
|
|
|
|
// do this to update the UI |
|
|
|
|
|
// eslint-disable-next-line no-self-assign |
|
|
|
|
|
this.givenToMeTotals = this.givenToMeTotals; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
let userMessage = "There was an error. See logs for more info."; |
|
|
|
|
|
const serverError = error as AxiosError; |
|
|
|
|
|
if (serverError) { |
|
|
|
|
|
this.isAlertVisible = true; |
|
|
|
|
|
if (serverError.message) { |
|
|
|
|
|
this.alertTitle = "User Message"; |
|
|
|
|
|
userMessage = serverError.message; // This is info for the user. |
|
|
|
|
|
this.alertMessage = userMessage; |
|
|
|
|
|
} else { |
|
|
|
|
|
this.alertTitle = "Server Message"; |
|
|
|
|
|
this.alertMessage = JSON.stringify(serverError.toJSON()); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
console.log("Here's the full error trying to save the claim:", error); |
|
|
|
|
|
this.alertTitle = "Claim Error"; |
|
|
|
|
|
this.alertMessage = error as string; |
|
|
|
|
|
} |
|
|
|
|
|
// Now set that error for the user to see. |
|
|
|
|
|
this.errorMessage = userMessage; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</script> |
|
|