From c017e787ff4b4c377105bf781c0970a4b1d2bc43 Mon Sep 17 00:00:00 2001 From: patnorris Date: Sun, 25 Feb 2024 15:33:17 +0100 Subject: [PATCH 1/5] Add personal note to donation record view if it exists --- .../donation_tracker_canister.did | 3 ++- .../donation_tracker_canister.did.d.ts | 3 ++- .../donation_tracker_canister.did.js | 3 ++- .../src/donation_frontend/components/DonationRecord.svelte | 7 ++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did index c8279d9..0277bdc 100644 --- a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did +++ b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did @@ -144,6 +144,7 @@ type DonationTracker = getTxidstext: () -> (TxidstextResult); getUTXOS: () -> (GetUtxosResponseResult); initRecipients: () -> (InitRecipientsResult); + isControllerLogicOk: () -> (AuthRecordResult); listRecipients: (RecipientFilter) -> (RecipientsResult) query; makeDonation: (DonationRecord) -> (DtiResult); submitSignUpForm: (SignUpFormInput) -> (text); @@ -190,7 +191,7 @@ type Donation = allocation: DonationCategories; donor: DonorType; dti: DTI; - hasBeenDistributed: bool; + hasBeenDistributed: opt bool; paymentTransactionId: PaymentTransactionId; paymentType: PaymentType; personalNote: opt text; diff --git a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts index 7f0459c..c1cd677 100644 --- a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts +++ b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts @@ -27,7 +27,7 @@ export interface Donation { 'dti' : DTI, 'rewardsHaveBeenClaimed' : boolean, 'paymentTransactionId' : PaymentTransactionId, - 'hasBeenDistributed' : boolean, + 'hasBeenDistributed' : [] | [boolean], 'totalAmount' : Satoshi, 'timestamp' : bigint, 'paymentType' : PaymentType, @@ -87,6 +87,7 @@ export interface DonationTracker { 'getTxidstext' : ActorMethod<[], TxidstextResult>, 'getUTXOS' : ActorMethod<[], GetUtxosResponseResult>, 'initRecipients' : ActorMethod<[], InitRecipientsResult>, + 'isControllerLogicOk' : ActorMethod<[], AuthRecordResult>, 'listRecipients' : ActorMethod<[RecipientFilter], RecipientsResult>, 'makeDonation' : ActorMethod<[DonationRecord], DtiResult>, 'submitSignUpForm' : ActorMethod<[SignUpFormInput], string>, diff --git a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js index caf9f6a..a2c5db8 100644 --- a/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js +++ b/frontend/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js @@ -42,7 +42,7 @@ export const idlFactory = ({ IDL }) => { 'dti' : DTI, 'rewardsHaveBeenClaimed' : IDL.Bool, 'paymentTransactionId' : PaymentTransactionId, - 'hasBeenDistributed' : IDL.Bool, + 'hasBeenDistributed' : IDL.Opt(IDL.Bool), 'totalAmount' : Satoshi, 'timestamp' : IDL.Nat64, 'paymentType' : PaymentType, @@ -220,6 +220,7 @@ export const idlFactory = ({ IDL }) => { 'getTxidstext' : IDL.Func([], [TxidstextResult], []), 'getUTXOS' : IDL.Func([], [GetUtxosResponseResult], []), 'initRecipients' : IDL.Func([], [InitRecipientsResult], []), + 'isControllerLogicOk' : IDL.Func([], [AuthRecordResult], []), 'listRecipients' : IDL.Func( [RecipientFilter], [RecipientsResult], diff --git a/frontend/src/donation_frontend/components/DonationRecord.svelte b/frontend/src/donation_frontend/components/DonationRecord.svelte index ab3bf60..b2788a3 100644 --- a/frontend/src/donation_frontend/components/DonationRecord.svelte +++ b/frontend/src/donation_frontend/components/DonationRecord.svelte @@ -15,7 +15,7 @@
-
+

Total Amount: {donation.totalAmount} {Object.keys(donation.paymentType)[0] === "BTC" ? "Satoshi" : ""}

Payment Type: {Object.keys(donation.paymentType)[0]}

{#if Object.keys(donation.paymentType)[0] === "BTC"} @@ -44,6 +44,11 @@ {#each Object.entries(donation.allocation) as [category, categoryValues], index}

{categoryNameTranslator[category]}: {categoryValues}

{/each} + {#if donation.personalNote[0]} + +

Personal Note: {donation.personalNote[0]}

+
+ {/if}
From 0234312aab3603eab9637265b86a4e3e943ec50f Mon Sep 17 00:00:00 2001 From: patnorris Date: Sun, 25 Feb 2024 15:34:13 +0100 Subject: [PATCH 2/5] Make new attribute optional for compatibility --- backend/donation_tracker_canister/src/Main.mo | 2 +- backend/donation_tracker_canister/src/Types.mo | 2 +- .../donation_tracker_canister/donation_tracker_canister.did | 2 +- .../donation_tracker_canister.did.d.ts | 5 +---- .../donation_tracker_canister.did.js | 2 +- .../src/declarations/donation_tracker_canister/index.js | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/donation_tracker_canister/src/Main.mo b/backend/donation_tracker_canister/src/Main.mo index 848eafa..eb24f4f 100644 --- a/backend/donation_tracker_canister/src/Main.mo +++ b/backend/donation_tracker_canister/src/Main.mo @@ -215,7 +215,7 @@ actor class DonationTracker(_donation_canister_id : Text) { donor : Types.DonorType = newDonor; personalNote : ?Text = donationInput.personalNote; // Optional field for personal note from donor to recipient rewardsHaveBeenClaimed : Bool = false; - hasBeenDistributed : Bool = false; // TODO: placeholder for future functionality + hasBeenDistributed : ?Bool = ?false; // TODO: placeholder for future functionality }; let newDonationResult = donations.add(newDonation); diff --git a/backend/donation_tracker_canister/src/Types.mo b/backend/donation_tracker_canister/src/Types.mo index 7347eea..05b50d0 100644 --- a/backend/donation_tracker_canister/src/Types.mo +++ b/backend/donation_tracker_canister/src/Types.mo @@ -62,7 +62,7 @@ module Types { donor : DonorType; personalNote : ?Text; // Optional field for personal note from donor to recipient rewardsHaveBeenClaimed : Bool; - hasBeenDistributed : Bool; // TODO: placeholder for future functionality + hasBeenDistributed : ?Bool; // TODO: placeholder for future functionality }; //------------------------------------------------------------------------- diff --git a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did index 5e2c7a5..0277bdc 100644 --- a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did +++ b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did @@ -191,7 +191,7 @@ type Donation = allocation: DonationCategories; donor: DonorType; dti: DTI; - hasBeenDistributed: bool; + hasBeenDistributed: opt bool; paymentTransactionId: PaymentTransactionId; paymentType: PaymentType; personalNote: opt text; diff --git a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts index dac73ed..c1cd677 100644 --- a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts +++ b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.d.ts @@ -1,6 +1,5 @@ import type { Principal } from '@dfinity/principal'; import type { ActorMethod } from '@dfinity/agent'; -import type { IDL } from '@dfinity/candid'; export type ApiError = { 'InvalidId' : null } | { 'ZeroAddress' : null } | @@ -28,7 +27,7 @@ export interface Donation { 'dti' : DTI, 'rewardsHaveBeenClaimed' : boolean, 'paymentTransactionId' : PaymentTransactionId, - 'hasBeenDistributed' : boolean, + 'hasBeenDistributed' : [] | [boolean], 'totalAmount' : Satoshi, 'timestamp' : bigint, 'paymentType' : PaymentType, @@ -183,5 +182,3 @@ export interface Utxo { 'outpoint' : OutPoint, } export interface _SERVICE extends DonationTracker {} -export declare const idlFactory: IDL.InterfaceFactory; -export declare const init: ({ IDL }: { IDL: IDL }) => IDL.Type[]; diff --git a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js index 0347110..a2c5db8 100644 --- a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js +++ b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/donation_tracker_canister.did.js @@ -42,7 +42,7 @@ export const idlFactory = ({ IDL }) => { 'dti' : DTI, 'rewardsHaveBeenClaimed' : IDL.Bool, 'paymentTransactionId' : PaymentTransactionId, - 'hasBeenDistributed' : IDL.Bool, + 'hasBeenDistributed' : IDL.Opt(IDL.Bool), 'totalAmount' : Satoshi, 'timestamp' : IDL.Nat64, 'paymentType' : PaymentType, diff --git a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/index.js b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/index.js index a184c43..9f873e6 100644 --- a/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/index.js +++ b/backend/donation_tracker_canister/src/declarations/donation_tracker_canister/index.js @@ -40,4 +40,4 @@ export const createActor = (canisterId, options = {}) => { }); }; -export const donation_tracker_canister = canisterId ? createActor(canisterId) : undefined; +export const donation_tracker_canister = createActor(canisterId); From b4fae8c0b7ad3d8e0f7a1f7ac79adc1fa5fdc7cc Mon Sep 17 00:00:00 2001 From: patnorris Date: Sun, 25 Feb 2024 15:48:07 +0100 Subject: [PATCH 3/5] Update param for optional value --- .../components/DonationCreationProcess/ConfirmationStep.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/donation_frontend/components/DonationCreationProcess/ConfirmationStep.svelte b/frontend/src/donation_frontend/components/DonationCreationProcess/ConfirmationStep.svelte index 35575f9..f083eb8 100644 --- a/frontend/src/donation_frontend/components/DonationCreationProcess/ConfirmationStep.svelte +++ b/frontend/src/donation_frontend/components/DonationCreationProcess/ConfirmationStep.svelte @@ -67,7 +67,7 @@ timestamp: 0n, dti: 0n, rewardsHaveBeenClaimed: false, - hasBeenDistributed: false, + hasBeenDistributed: [false], donor: { Anonymous: null }, From 17b207e645ae61a8286156b4fb35e5b08824166b Mon Sep 17 00:00:00 2001 From: patnorris Date: Sun, 25 Feb 2024 16:22:10 +0100 Subject: [PATCH 4/5] Make data structure stable --- backend/donation_tracker_canister/src/Main.mo | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/donation_tracker_canister/src/Main.mo b/backend/donation_tracker_canister/src/Main.mo index eb24f4f..05f4625 100644 --- a/backend/donation_tracker_canister/src/Main.mo +++ b/backend/donation_tracker_canister/src/Main.mo @@ -46,10 +46,8 @@ actor class DonationTracker(_donation_canister_id : Text) { stable var donationsStable : [Donation] = []; // Map each principal to a list of donation indices (DTIs) - private var donationsByPrincipal = HashMap.HashMap>(0, Principal.equal, Principal.hash); - //TODO: stable var donationsByPrincipalStable : [(Principal, Buffer.Buffer)] = []; - // Alternative: use [DTI] instead of Buffer.Buffer; less efficient but straightforward to make stable - // Or: stable Buffer implementation? + private var donationsByPrincipal = HashMap.HashMap(0, Principal.equal, Principal.hash); + stable var donationsByPrincipalStable : [(Principal, [DTI])] = []; // Alternative: Buffer.Buffer instead of [DTI]; more efficient but less straightforward to make stable // Store recipients and map each recipientId to the corresponding Recipient record private var recipientsById = HashMap.HashMap(0, Text.equal, Text.hash); @@ -222,12 +220,12 @@ actor class DonationTracker(_donation_canister_id : Text) { // Update the map for the caller's principal if (Principal.isAnonymous(msg.caller)) {} else { - let existingDonations = switch (donationsByPrincipal.get(msg.caller)) { - case (null) { Buffer.Buffer(0) }; + let existingDonations : [DTI] = switch (donationsByPrincipal.get(msg.caller)) { + case (null) { [] }; case (?ds) { ds }; }; - let addDonationResult = existingDonations.add(newDti); - donationsByPrincipal.put(msg.caller, existingDonations); + let addDonationResult : [DTI] = Array.append(existingDonations, [newDti]); + donationsByPrincipal.put(msg.caller, addDonationResult); }; let associatedDonations = switch (donationsByTxId.get(donationInput.paymentTransactionId)) { @@ -273,12 +271,12 @@ actor class DonationTracker(_donation_canister_id : Text) { // No donations found return #Ok({ donations = [] }); }; - case (?dtiBuffer) { + case (?dtiArray) { // Donations found for user - let dtis : [DTI] = Buffer.toArray(dtiBuffer); + let dtis : [DTI] = dtiArray; // Iterate over dtis, get donation for each dti // push to return array - let userDonations : Buffer.Buffer = Buffer.Buffer(dtiBuffer.capacity()); + let userDonations : Buffer.Buffer = Buffer.Buffer(dtiArray.size()); for (i : Nat in dtis.keys()) { userDonations.add(donations.get(dtis[i])); }; @@ -587,7 +585,7 @@ actor class DonationTracker(_donation_canister_id : Text) { system func preupgrade() { // Copy the runtime state back into the stable variable before upgrade. donationsStable := Buffer.toArray(donations); - //TODO: donationsByPrincipalStable := Iter.toArray(donationsByPrincipal.entries()); + donationsByPrincipalStable := Iter.toArray(donationsByPrincipal.entries()); recipientsByIdStable := Iter.toArray(recipientsById.entries()); studentsBySchoolStable := Iter.toArray(studentsBySchool.entries()); donationsByTxIdStable := Iter.toArray(donationsByTxId.entries()); @@ -599,8 +597,8 @@ actor class DonationTracker(_donation_canister_id : Text) { // After upgrade, reload the runtime state from the stable variable. donations := Buffer.fromArray(donationsStable); donationsStable := []; - //TODO: donationsByPrincipal := HashMap.fromIter(Iter.fromArray(donationsByPrincipalStable), donationsByPrincipalStable.size(), Text.equal, Text.hash); - //TODO: donationsByPrincipalStable := []; + donationsByPrincipal := HashMap.fromIter(Iter.fromArray(donationsByPrincipalStable), donationsByPrincipalStable.size(), Principal.equal, Principal.hash); + donationsByPrincipalStable := []; recipientsById := HashMap.fromIter(Iter.fromArray(recipientsByIdStable), recipientsByIdStable.size(), Text.equal, Text.hash); recipientsByIdStable := []; studentsBySchool := HashMap.fromIter(Iter.fromArray(studentsBySchoolStable), studentsBySchoolStable.size(), Text.equal, Text.hash); From e0d1506bf05680d05f7e30129c5b0221d7de3cb2 Mon Sep 17 00:00:00 2001 From: patnorris Date: Sun, 25 Feb 2024 17:09:54 +0100 Subject: [PATCH 5/5] Add input checks --- backend/donation_tracker_canister/src/Main.mo | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/backend/donation_tracker_canister/src/Main.mo b/backend/donation_tracker_canister/src/Main.mo index 05f4625..f2edad6 100644 --- a/backend/donation_tracker_canister/src/Main.mo +++ b/backend/donation_tracker_canister/src/Main.mo @@ -191,9 +191,79 @@ actor class DonationTracker(_donation_canister_id : Text) { }; + private func verifyDonationInput(donationInput : Donation) : async Bool { + // Perform basic checks + // Total amount has to be a positive number + if (donationInput.totalAmount <= 0) { + return false; + }; + + // Verify that recipientId exists + switch (recipientsById.get(donationInput.recipientId)) { + case (null) { return false; }; + case (?recipient) { }; + }; + + // Verify valid allocation + var totalAllocated = donationInput.allocation.curriculumDesign; + totalAllocated += donationInput.allocation.teacherSupport; + totalAllocated += donationInput.allocation.schoolSupplies; + totalAllocated += donationInput.allocation.lunchAndSnacks; + if (totalAllocated != donationInput.totalAmount) { + return false; + }; + + // Check that personalNote is not longer than 100 characters + switch (donationInput.personalNote) { + case (null) { }; + case (?note) { + if (note.size() > 100) { + return false; + }; + }; + }; + + // Elaborate check whether paymentType is valid and paymentTransactionId exists and can be used + switch (donationInput.paymentType) { + case (#BTC) { + // Verify the Bitcoin transaction + try { + let txCheckResult = await getBtcTransactionDetails({bitcoinTransactionId = donationInput.paymentTransactionId}); + switch (txCheckResult) { + case (#Ok(bitcoinTransactionRecord)) { + // bitcoinTransactionId was found and has value + let bitcoinTransaction = bitcoinTransactionRecord.bitcoinTransaction; + // Check that value left on transaction is high enough to donate totalAmount + let valueLeft = bitcoinTransaction.totalValue - bitcoinTransaction.valueDonated; + if (valueLeft <= 0) { + // the transaction doesn't have any value left to donate + return false; + } else if (valueLeft < donationInput.totalAmount) { + // the transaction doesn't have enough value left to donate totalAmount + return false; + }; + }; + case (_) { return false; }; // bitcoinTransactionId wasn't found or doesn't have value bigger 0 + }; + } catch (error : Error) { + return false; + }; + }; + // Handle other payment types as they are added + case (_) { return false; }; // Fallback: unsupported paymentType + }; + + // All checks were successful and the donation input is valid + return true; + }; + public shared (msg) func makeDonation(donationRecord : Types.DonationRecord) : async Types.DtiResult { let donationInput = donationRecord.donation; // Potential TODO: checks on inputs + let donationInputIsValid : Bool = await verifyDonationInput(donationInput); + if(not donationInputIsValid) { + return #Err(#Other("Invalid Donation input")); + }; let newDti = donations.size(); // Simply use index into donations Array as the DTI var newDonor : Types.DonorType = #Anonymous;