-
Notifications
You must be signed in to change notification settings - Fork 24
ICRC-2: Approve and Transfer From #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4c59fd2
c6839cc
57c4989
b4a1e60
c9a8d31
b7738a7
2c8fda0
6f064ad
fe98141
ffcd688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,15 @@ | ||
| { | ||
| "version": 1, | ||
| "dfx": "0.11.2", | ||
| "canisters": { | ||
| "icrc1_example": { | ||
| "type": "motoko", | ||
| "main": "src/icrc1/main.mo" | ||
| "main": "icrc1/main.mo" | ||
| } | ||
| }, | ||
| "defaults": { | ||
| "build": { | ||
| "packtool": "", | ||
| "packtool": "mops sources", | ||
| "args": "" | ||
| } | ||
| }, | ||
|
|
||
| "networks": { | ||
| "local": { | ||
| "bind": "127.0.0.1:8000", | ||
| "type": "ephemeral" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [dependencies] | ||
| base = "0.8.7" | ||
| icrc1 = "0.0.1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import Blob "mo:base/Blob"; | ||
| import Debug "mo:base/Debug"; | ||
| import Int "mo:base/Int"; | ||
| import Nat "mo:base/Nat"; | ||
| import Nat64 "mo:base/Nat64"; | ||
| import Option "mo:base/Option"; | ||
| import Result "mo:base/Result"; | ||
| import Time "mo:base/Time"; | ||
|
|
||
| import STMap "mo:StableTrieMap"; | ||
|
|
||
| import T "Types"; | ||
| import Utils "Utils"; | ||
| import Account "../ICRC1/Account"; | ||
| import Transfer "../ICRC1/Transfer"; | ||
|
|
||
| module { | ||
|
|
||
| // Checks if an approval expiration is greater than the current ledger time | ||
| public func validate_expiration(token : T.TokenData, expires_at : ?Nat64) : Bool { | ||
| switch (expires_at) { | ||
| case null { return true }; | ||
| case (?expiration) { | ||
| return Transfer.is_in_future(token, expiration); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| /// Checks if an approve request is valid | ||
| public func validate_request( | ||
| token : T.TokenData, | ||
| app_req : T.ApproveRequest, | ||
| ) : Result.Result<(), T.ApproveError> { | ||
|
|
||
| if (app_req.from.owner == app_req.spender.owner) { | ||
| return #err( | ||
| #GenericError({ | ||
| error_code = 0; | ||
| message = "The spender account owner cannot be equal to the source account owner."; | ||
| }) | ||
| ); | ||
| }; | ||
|
|
||
| if (not Account.validate(app_req.from)) { | ||
| return #err( | ||
| #GenericError({ | ||
| error_code = 0; | ||
| message = "Invalid account entered for approval source. " # debug_show (app_req.from); | ||
| }) | ||
| ); | ||
| }; | ||
|
|
||
| if (not Account.validate(app_req.spender)) { | ||
| return #err( | ||
| #GenericError({ | ||
| error_code = 0; | ||
| message = "Invalid account entered for approval spender. " # debug_show (app_req.spender); | ||
| }) | ||
| ); | ||
| }; | ||
|
|
||
| // TODO: Verify if approval memo should be validated for approvals. | ||
| if (not Transfer.validate_memo(app_req.memo)) { | ||
| return #err( | ||
| #GenericError({ | ||
| error_code = 0; | ||
| message = "Memo must not be more than 32 bytes"; | ||
| }) | ||
| ); | ||
| }; | ||
|
Comment on lines
+62
to
+70
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yh, validating the memo here is fine. Ideally, an approval request should function under the same conditions as a transfer request. |
||
|
|
||
| // TODO: Verify if approval fee should be validated as a transfer fee. | ||
| if (not Transfer.validate_fee(token, app_req.fee)) { | ||
| return #err( | ||
| #BadFee { | ||
| expected_fee = token._fee; | ||
| } | ||
| ); | ||
| }; | ||
|
Comment on lines
+72
to
+79
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the approval fee, we can add a specific field in the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at the |
||
|
|
||
| let balance : T.Balance = Utils.get_balance( | ||
| token.accounts, | ||
| app_req.encoded.from, | ||
| ); | ||
|
|
||
| // If no approval fee provided, validates against transaction fee. | ||
| if(Option.get(app_req.fee, token._fee) > balance){ | ||
| return #err(#InsufficientFunds { balance }); | ||
| }; | ||
|
|
||
| // Validates that the approval contains the expected allowance | ||
| switch (app_req.expected_allowance) { | ||
| case null {}; | ||
| case (?expected) { | ||
| let allowance_record = Utils.get_allowance(token.approvals, app_req.encoded); | ||
| if (expected != allowance_record.allowance) { | ||
| return #err( | ||
| #AllowanceChanged { | ||
| current_allowance = allowance_record.allowance; | ||
| } | ||
| ); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| if (not validate_expiration(token, app_req.expires_at)) { | ||
| return #err( | ||
| #Expired { | ||
| ledger_time = Nat64.fromNat(Int.abs(Time.now())); | ||
| } | ||
| ); | ||
| }; | ||
|
|
||
| switch (app_req.created_at_time) { | ||
| case (null) {}; | ||
| case (?created_at_time) { | ||
|
|
||
| if (Transfer.is_too_old(token, created_at_time)) { | ||
| return #err(#TooOld); | ||
| }; | ||
|
|
||
| if (Transfer.is_in_future(token, created_at_time)) { | ||
| return #err( | ||
| #CreatedInFuture { | ||
| ledger_time = Nat64.fromNat(Int.abs(Time.now())); | ||
| } | ||
| ); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| #ok(); | ||
| }; | ||
|
|
||
| /// Writes/overwrites the allowance of an approval | ||
| public func write_approval( | ||
| token : T.TokenData, | ||
| app_req : T.WriteApproveRequest, | ||
| ) : async* T.ApproveResult { | ||
|
|
||
| let { amount = allowance; expires_at; encoded } = app_req; | ||
| let prev_allowance = Utils.get_allowance(token.approvals, encoded); | ||
| let new_allowance : T.Allowance = { allowance; expires_at }; | ||
|
|
||
| if (new_allowance != prev_allowance) { | ||
| let accont_approvals = Utils.get_account_approvals(token.approvals, encoded.from); | ||
| switch (accont_approvals) { | ||
| case (?approvals) { | ||
| let spenders = STMap.get(approvals, Blob.equal, Blob.hash, encoded.spender); | ||
| STMap.put(approvals, Blob.equal, Blob.hash, encoded.spender, new_allowance); | ||
| }; | ||
| case null { | ||
| let approvals : T.Approvals = STMap.new(); | ||
| STMap.put(approvals, Blob.equal, Blob.hash, encoded.spender, new_allowance); | ||
| STMap.put(token.approvals, Blob.equal, Blob.hash, encoded.from, approvals); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| #Ok(allowance); | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can update this to prevent the minting account from making approval requests or being included as the spender in approval requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After giving it more thought, I think it would be best to leave this decision to the token owner. I currently can't think of a use case where the
minting_accountpermits other accounts to mint tokens but some may come up in the future.