Open timohanke opened 5 months ago
Very happy to see this as I've almost written it a couple dozen times. Lots of great thoughts.
A few thoughts:
type Token = record {
canister: Principal;
asset_id: opt nat;
};
Why? For ICRC1/2 tokens the id will always be null, but if there are future multi-token canisters(much like you designed for HPL). Adding this now will make it easier to use in the future without refactoring or having to issue a new ICRC.
icrc84_supported_tokens
and icrc84_all_credits
could benefit from adding the paging syntax used in ICRC-7. (prev: opt Item, take: opt nat)
which will let the standard support responses that go over 2MB. Maybe seems silly, but I think I saw that Solona had something crazy like 475,000 tokens generated for it over the course of a couple of months. This syntax allows for paginating but may require the statement that the items should be returned in some deterministic order.
notify - I understand the reasoning that you want to not have to worry with icrc3 and tracking burned blocks, but I have concerns about the alternative where an account may become 'locked' by the lock not being released if something goes haywire. I'm sure we can find a good programmatic solution, maybe with a time-out or something, but curious if you have run across that or have any concerns.
ICRC-10...need to reference the ICRC-10 key to use for support detection. https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-10/ICRC-10.md
Secrets. I don't like 'em. :) It would be nice to have either a separate ICRC for 84 block types for ICRC-3, or perhaps we can put them here and mark their use as optional?
@skilesare Thanks for reviewing this and thanks for your interesting comments. I would love to discuss them further.
Re 1. Any future multi-token ledger, HPL or other, will not comply with ICRC-1 because the ICRC-1 itself does not allow to specify an asset id or similar in its interface. So we have to decide if this standard (icrc-84) is supposed to be icrc-1 specific or if its more general. I thought for a while to make it more general and at that time wanted to specify the token in the form of a variant like #icrc1 : principal
which would leave room for future extensions to other tokens. But then I thought that all the other functions for deposit/withdrawal, both from an interface perspective as well as from a behaviour perspective, are kind of icrc-1 specific. Without knowing how a future multi-token ledger would work and how its interface looks like it would be hard to design this present proposal in a future proof way. So I decided to drop that path and stay icrc-1 specific with the goal of staying simple and not over-designing it. That being said I would be very interested in people's opinion on this question in general.
At the same time, it is not wise to forego easy ways that can buy us extendability. So for the token specification we would either make it a variant (like #icrc1 : principal
) or, as you propose, a record. Along the lines of your proposal the easiest would to just wrap the token principal in a record like record { ledger : principal }
. We don't even have to specify the second field now because it can just be added in later. If the new field is opt
then candid subtyping will automatically set it to null
if the caller does not specify it. I would propose as a first step to do this wrapping.
Re. 2 for icrc84_supported_tokens
. I was expecting this question to come up during the discussion. The question is even if that many tokens on ICP if a single service would necessarily support that many. With 2MB (or is it 3MB for external query calls?) at say 200 bytes per token we can support up to 10k tokens in a service. Probably the answer will be yes we need pagination.
As for the order, what exactly is the requirement? Is it only that the order is deterministic (as in for example alphabetical order)? Or is it append-only so that newly added tokens do not change the order of older tokens (as in for example chronological order, the order in which tokens are added to the service)? I would expect that for pagination to be useful for the caller the order has to be the chronological one.
I am wondering what requirements we create for the implementation, hence the question. For example, if the supported tokens are stored in a tree then the type of tree has to be chosen so that the order matches the iteration order when iterating through the tree entries. Or there need to be two data structures, a tree for lookup and a vector for enumeration. It is not necessarily a problem, we just have to be aware of it when we add pagination.
And we have to be aware that we may be exposing an implementation detail. For example a token principal may now have an "index" in the sequence which also uniquely identifies it (in this service at least).
Re. 2 for icrc84_all_credits
. Similar questions apply as above with the some additional considerations:
supported_tokens
then there are now gaps in that sequence because we don't return entries that have a balance of 0. Does the pagination index refer to the index in the sequence before or after removal of gaps?supported_tokens
.There is also a different approach possible which is an alternative approach to pagination. It is if the caller provides, in an argument, the list of tokens that he wants to query. Then the caller can make sure the list isn't too long and can split a long list over two calls. It is also more flexible if the user interested in an update of just a few balances, not all. However, it cannot be used if the caller has forgotten which tokens he owns and wants to know all balances that he owns. So there is a trade-off.
Re. 3 (notify/lock). Are you saying that any implementation will necessarily require locks? (probably true, just asking) Can you elaborate more on this issue? For example are you afraid of a lock not getting released because of bugs? Or because of calls not responding? By IC issues or by a token ledger being malicious/buggy? What would be the impact on the services if a lock does not get released? Does an approach based on transaction history really avoid all these problems? I mean a response not coming back would block upgrades even with that approach.
Re 4. What do you mean by ICRC-10 key? I see the ICRC-10 record has a name
and a url
and the name is given (must be "ICRC-84"). Do you mean the url?
Re. 5. Can you elaborate more please? ICRC-3 is for "events" not for balances, right? What events are secret here that you would like to see ICRC-3 block types for?
Re 1.
record { ledger : principal }
I think this might make the most sense, but using a variant might be more complete. I'd be open to either. Apparently, the new motoko compiler will do way with having to null the Variants to make them backwards and forwards compatible, so that might be the easiest.
I'd suggest #icrc1: Principal
. Later if we have something like #icrcHPL: record { ledger: Principal;asset_id: Nat}
it will be expandable and explicit.
Not to confuse too many issues, but if we did actually do #icrc1 and #icrc2 we could specifically reject icrc2 notify attempts and/or icrc1 deposit attempts. I guess those will fail gracefully anyway, but pushing people down the transfer from route probably makes the code cleaner and avoids locks if it is available.(Just thinking out loud here....I think just having #icrc1 is fine for now.
Re. 2
For example, if the supported tokens are stored in a tree then the type of tree has to be chosen so that the order matches the iteration order when iterating through the tree entries. Or there need to be two data structures, a tree for lookup and a vector for enumeration. It is not necessarily a problem, we just have to be aware of it when we add pagination.
This pops up a ton when discussing these standards and I've had to go back and retrofit a few indexes into some things that I didn't really want to do, but we probably want consistent, deterministic ordering that makes sense to both the publisher and consumer. Supported tokens seems like it would be either alpha by ledger canister id or binary order by canister ID....the Token objects we have don't have a symbol saved in the data, or I would say that makes the most sense. Likely not a huge deal here at the moment....I think it may be safe to says MUST return items in a "consistent order" and leave the exact order up to the implementation.
Re. 2 and icrc84_all_credits
I think you just want to return items with a non-0 credit balance, and again, using a "consistent order". You just want to make sure that the consumer is able to get all the items by going from front to back. Obviously it is possible for something to get inserted up at the front of the queue while querying if there are bunch of pages, but I don't think there is much that we can do about that.
Re. 3
Locks are probably the answer for this if you don't want to do burned blocks. Basically, my brain has this block that occurs when I start thinking about all the ways the IC could fail and squaring it with all the tech there to make sure that doesn't happen(and that is before I get stressed out about my own bugs). Of course, a canister can be in the middle of a upgrade, or a subnet could be really slow, or the output queue could get full and time out after five minutes or something.
I was doing an implementation of withdraw and I avoided a lock by burning the balance before trying to send out the tokens and then minting them back if something fails. I think this works well and avoids a double withdraw.
Notify is a bit more problematic as you don't want rapid requests getting into a race condition. Probably better to just put some kind of lock on that account until you get some response back and have a clean up timer that runs once a day just in case something odd happens. Again...just thinking out loud here. Ideally I'd like to just not have notify and force everyone through approvals and in the contract I'm building now it always uses ICP so I know that is there.
Re 4 ICRC-10
I'm just saying that the standard should specifically say that an ICRC-10 record indicating support for icrc-84 should be added so that wallets and other services can detect that it is supported.
Basically just copying this section and changing it to icrc84 https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md#icrc10_supported_standards
Re. 5
I'm just posing the question that if an ICRC-84 service doesn't have any reason to keep credit balances a secret and wants to publish a complete transaction log of all activity on the server, it would be nice to have the ICRC-3 block schemas defined somewhere.
For example, for ICRC-72 we pulled the block schemas out and moved them to ICRC-83 as there are likely many event systems that won't want to log every state change to the service, but for those that do, we provide the schemas: https://github.com/icdevs/ICEventsWG/blob/main/Meetings/20240529/icrc83draft.md (this is still an early draft)
A few implementation thoughts:
An ICRC-84 service does not have subaccount. This means that if a canister wants to use it, it will need to co-mingle user funds in the same set of credits without a way to distinguish who's credits are who's on a per token basis.
Use case: I have written a bot that monitors prices across the IC and I offer a menu of possible actions that users can configure their actions to draw from(see defi vectors https://github.com/Neutrinomic/defivectors). In this case, the vector canister would need to keep all funds in a default account and track them locally.
This may be by design, but I thought it was worth stating out loud.
I was trying to implement it for use in a simple 'wrapped token' contract that tracks cycles for a tokenomics use case and then lets users withdraw them to the new cycles ledger. I didn't need deposits or notify because the balances are pushed in from the behavior of other users. Once the balances were there though, I had them going to standard ICRC1 type accounts.....this meant that if _credits and _trackedDeposit only work for the default account. Not a huge deal, but maybe there is a solution for this?
Derived canister IDs would help for this if there were services that wanted to use this kind of system from one canister while managing many user's accounts.
For withdrawal, I have the workaround that it will withdraw from the same icrc1 account as specified in the to field, but this obviously restricts some usefulness as you can't direct your funds to a different account.
I have a bit of an alternative take on this kind of multi token deposit system that I've been working on and it is now about third down on my todo list...I wanted to take a quick turn at implementation before I released much about it, but perhaps we can compare notes. It has the same kind of restriction that you lose multi-subaccount functionality
I'd suggest
#icrc1: Principal
. Later if we have something like#icrcHPL: record { ledger: Principal;asset_id: Nat}
it will be expandable and explicit.
It is not that easy. Just changing the Token
type from principal
to variant { ledger : principal }
is not enough. For example, look at the argument type for the deposit
function:
type DepositArgs = record {
token : Token;
amount : Amount;
subaccount : opt Subaccount; // subaccount of the caller which has the allowance
};
A different ledger (take for example HPL) may not have subaccounts but may need some other fields to specify a pull transfer. The same holds for the argument of the withdrawal
function.
We would have to wrap the whole argument list into a variant with case #icrc1
, not just the Token. In summary we have to take all functions defined by this standard and for each of them wrap the entire argument list into a variant { icrc1 : record {..} }
. Which raises the question why not make a new standard for each type of ledger?
Re. pagination for supported_tokens
. At any point in time, and between calls, new supported tokens can get added. Let's suppose also that supported tokens can get removed. On the other hand, adding or removing supported tokens is infrequent enough so that the probability of it happening between subsequent paginated calls from the same user is low. So what if supported_tokens
returns the timestamp from when the internal list was last updated? The user can then interpret the situation. If the user gets all pages back and they all contain the same timestamp then he knows the pages are consistent. If the timestamps differ then the user has to start querying all pages again.
Without such a mechanism the user could accidentally miss some supported tokens.
Ideally I'd like to just not have notify and force everyone through approvals and in the contract I'm building now it always uses ICP so I know that is there.
Approvals require the user to make this additional step of setting up an allowance. It's an additional friction for someone who quickly wants to try out the service for the first time. He cannot deposit from an exchange. And most wallets don't support setting allowances yet, or do they? Moreover, you need to properly use an allowance with a subaccount built into it (not just a default allowance), for otherwise the service will just credit the funds to the owner of the allowance. That doesn't help the user at all because the service will credit it to the user's wallet principal. But the user needs it credited to the principal that he is using in this service frontend. Is there even any wallet out there at the moment that supports non-default allowances (with a subaccount built into them)?
I think notify will be the go-to way for most users.
I'm just posing the question that if an ICRC-84 service doesn't have any reason to keep credit balances a secret and wants to publish a complete transaction log of all activity on the server, it would be nice to have the ICRC-3 block schemas defined somewhere.
So in our case that would be any changes to the internal credit balances, right? If this standard is about deposit and withdrawal then I think it should not be concerned about what services do internally with credits. If you want to log credit changes then that should be its own standard. That's a feature that is independent. It can even exists for a service that does not have deposits and withdrawals (but credits that are based on something else).
An ICRC-84 service does not have subaccount. This means that if a canister wants to use it, it will need to co-mingle user funds in the same set of credits without a way to distinguish who's credits are who's on a per token basis.
The scenario is where a service canister which has multiple users is the user of an icrc-84 compliant other service and your are comparing: a) the first service has one credit balance in the second service and internally tracks how this one credit maps to the different own users, b) the first service maintains multiple credit balances in the second service (via subaccounts) and maps them to his own users.
From a security perspective there seems to be no difference because the first service is always in full control of the credit. Maybe if the first service loses its data in an upgrade then the individual user credits are still maintained in the second service, is that the benefit?
The price we would pay for b) is that now every icrc84 service is forced to support credit subaccounts. This comes with implementation burden and resource burden. I doubt it is worth it for the gain.
With the current interface any icrc84 can already offer subaccounts on an opt-in basis. To give some context, the notify interface is already designed so that each service can define its own "actions" that a caller to notify can specify, so that the caller can make a single call that effectively becomes "notify_and_X" where X is some action (same for deposit_and_X, etc.). The original idea was to do save a second call and therefore save latency. But you can also use it to bind a specific deposit to a specific action. As an example, if the service is a minter, then notify_and_mint means I can make a deposit and mint a wrapped token in one call, saving latency for the user. For your use case, the service can offer notify_and_segregate which would mean to assign the new credit to a certain subaccount.
The "action" can simply be added as a new field to the NotifyArg
or DepositArg
record. For example:
type NotifyArg = record {
token : Token;
action : opt principal;
};
Services can just add their custom action to optional fields in the argument record. Action-specific results can be added as new fields to the corresponding response record.
Which raises the question why not make a new standard for each type of ledger?
Hmm...I'd say yes if things are that much different, but for example, Depositing to ICRC-7 could use this...and a couple other's that I've been looking at still use ICRC-1.Account formats....so I think would could at least expand it to those without having a new standard. In fact, adding ICRC-7 should be fairly easy, except that the possible values for credits are only 0 or 1.
So what if
supported_tokens
returns the timestamp from when the internal list was last updated?
This is a great idea and something I wish we'd thought of for ICRC-7 etc.😬
I think notify will be the go-to way for most users.
Good points here...I hope we'll see approve showing up in more wallets, etc....but you are right that it can be more burdensome if the user is not using a dapp to do this procedure.
That's a feature that is independent. It can even exists for a service that does not have deposits and withdrawals (but credits that are based on something else).
At some point I'm going to have to sit down and make a list of all these more generic block types...This is a good thought here and I'll put it on my list.
For your use case, the service can offer notify_and_segregate which would mean to assign the new credit to a certain subaccount.
Very nice...I like it! Thanks for the feedback.
Depositing to ICRC-7 could use this...and a couple other's that I've been looking at still use ICRC-1.Account formats....so I think would could at least expand it to those without having a new standard. In fact, adding ICRC-7 should be fairly easy, except that the possible values for credits are only 0 or 1.
Makes sense. Just double-checking: the single change of making type Token = record { ledger : principal }
is enough to later phase in ICRC-7 support, right? Or do we need anything else? As you said credit values are 0 or 1 so we don't need to change the Amount
type. Anything else that I am missing here?
So what if supported_tokens returns the timestamp from when the internal list was last updated?
This is a great idea and something I wish we'd thought of for ICRC-7 etc.😬
Pagination will probably require us to provide a separate function that returns the current number of items. Theoretically the caller can make sequential queries until he receives back a page of shorter length than the take
argument, at which point he knows he has reached the end. However, in practice the caller likely wants to know the number of item ahead of time. For example, he could be allocating an Array to store them. Or he might want to query for all pages concurrently rather than sequentially.
[WIP] ICRC-84: Deposit and Withdrawal Standard for ICRC-1 tokens
Financial service canisters use this standard to allow users to deposit ICRC-1 tokens and withdraw them again. An example for such a service is a DEX.
Tokens
The same service can accept deposits in one or more different ICRC-1 tokens. A token is uniquely identified by the principal of its ICRC-1 ledger.
The list of accepted tokens can be queried with the following function.
Amounts
Amounts are specified as
nat
in the smallest unit of the ICRC-1 token. Decimals do not play a role in the interface.To get the decimals the user has to query the ICRC-1 ledger.
Users
Users are identified by their principal.
Deposit accounts
There are two ways for a user to deposit funds to the service. The first one is via direct transfer to a deposit account. The second one is via an allowance, but only if the ICRC-1 ledger supports ICRC-2.
In the direct transfer method, users make deposits into individual deposit accounts which are subaccounts that are derived from the
User
principal in a deterministic and publicly known way. The derivation works by embedding the principal bytes right-aligned into the 32 subaccount bytes, pre-pending a length byte, and left-padding with zeros.Requirements
The only requirement on the underlying token ledger is the ICRC-1 standard. Since the standard deposit method is based on deposit accounts, not allowances, the ICRC-2 extension is not required. Moreover, as will become clear below, the deposit method is balance-based (as opposed to transaction-based). This means it is sufficient that the service can read the balances in the deposit accounts from the underlying token ledger. It is not required that the service can inspect individual deposit transactions by transaction id, memo or other means. Hence, it is not required that the underlying token ledger provides an indexer, transaction history or archive. In particular, the ICRC-3 extension is not required.
TokenInfo
For each token the service has the following public set of configuration parameters defined. The values may change over time.
deposit_fee
specifies the fee that is deducted each time a deposit is detected and consolidated into the service's main account. Thedeposit_fee
can but does not have to coincide with the transfer fee of the underlying ICRC-1 token. However, the application of thedeposit_fee
should coincide with actual transfers happening. For example, if the user makes multiple installments into the deposit account and then the service manages to consolidates them all at once into its main account then thedeposit_fee
should be charged only once. But still, the amount of thedeposit_fee
can differ from the underlying transfer fee charged by the ledger.withdrawal_fee
specifies the fee that is deducted when the user makes a withdrawal. Thewithdrawal_fee
can but does not have to coincide with the transfer fee of the underlying ICRC-1 token. It is charged for each withdrawal that a user makes and that results in a successful ICRC-1 transfer.min_deposit
is the minimal deposit that is considered valid by the service. Any balance in a deposit account that is below this value is ignored. For example, say for the ICP token a service has defineddeposit_fee = 20_000
andmin_deposit = 100_000
. If the user makes a deposit of exactly100_000
e8s then20_000
will be deducted and the user will be credited with80_000
e8s. The service will empty out the user's deposit account. As a result, the service will take in90_000
e8s because the ICP ledger's transfer fee is10_000
e8s. If instead the user had made a deposit of99,999
then it would have been ignored.min_deposit
must be larger thandeposit_fee
. For example, ifdeposit_fee = 20_000
thenmin_deposit
must be at least20_001
.min_withdrawal
is the minimal withdrawal that a user can make. Any withdrawal request below this amount will be denied.min_withdrawal
must be larger thanwithdrawal_fee
. For example, say for the ICP token a service has definedwithdrawal_fee = 20_000
andmin_withdrawal = 100_000
. If the user requests a withdrawal of exactly100_000
e8s then the user will be debited with100_000
e8s and the service will initiate a transfer of80_000
e8s to the user. As a result, the service will pay90_000
e8s because the ICP ledger's transfer fee is10_000
e8s.Note: The service will never make transfers of amount 0 on the ICRC-1 ledgers even though ICRC-1 technically allows them. This is true for consolidation of deposits and for withdrawals.
The token info can be queried with the following method.
If the specified
Token
is not supported by the service then the call will throw the async errorcanister_reject
with error message"UnknownToken"
.Credits
Credits are tracked by the service on a per-token basis. The unit for credits is the same as the unit of the corresponsing ICRC-1 token. However, credits are of slighly different nature than token balances even though the use the same unit. Credits are virtual and for greater flexibility we allow credits to go negative, hence we use type
int
.A user can query his personal credit balance with the following method.
If the specified
Token
is not supported by the service then the call will throw the async errorcanister_reject
with error message"UnknownToken"
.Credit balances are private. The above method returns the balance of the caller.
The service is not expected to distinguish non-existing users from existing ones with a credit balance of 0. If the caller is not known to the service, has never used the service before, or has never used the service for the given Token before then the method simply returns a value of zero.
For greater efficiency and to reduce query load, there is a method to obtain a user's credits in all tokens at once.
The returned vector contains all tokens for which the caller has a non-zero credit balance. The tokens with a zero credit balance are stripped from the response.
As before, a non-existing user is handled the same as a user with a zero balance in all tokens. In both cases an empty vector is returned.
Notification
There are two steps required when a user makes a deposit with the direct transfer method:
Then the service queries the ICRC-1 ledger for the balance in the deposit account and credits the user.
The second step is done via the following method.
where
A call to
icrc84_notify
notifies the service about a deposit into the deposit account of the caller for the specified token. The service is free to expand this record with additional optional fields to include an action that is to be done with the newly detected deposits.The result type is as follows.
If the specified
Token
is not supported by the service then the call will throw the async errorcanister_reject
with error message"UnknownToken"
.The service will make a downstream call to the underlying ICRC-1 ledger before returning to the user. If the downstream call fails then the variant
Err = CallLedgerError
is returned. The error message is not specified by this standard but is recommended to describe the async error that actually happened in the downstream call.The service is not expected to make concurrent downstream calls for the same balance. Hence, if the same caller calls
notify
twice concurrently for the sameToken
then the second call will returnErr = NotAvailable
. This error generally means thenotify
method is currently blocked for this caller and token, and that it should be retried later. The additional text error message returned withNotAvailable
is not specified by this standard.If the downstream call succeeds then the method will return the
Ok
record.The
deposit_inc
field is the incremental deposit amount that was detected relative to the last known deposit balance. If no new deposit was detected then a zero value is returned.Calls to notify are not idempotent. If the user makes one deposit transfer and then calls
notify
twice (with no additional transfer between the two calls tonotify
) then the first call will return a non-zerodeposit_inc
value and the second call will return zero.If the user makes two deposit transfers and then calls
notify
(with no additionalnotify
call between the two deposit transfers) thennotify
will return the sum of the two transfer amounts asdeposit_inc
.The
credit_inc
field is the incremental credit amount applied to the user as a result of this call. The value may be lower thandeposit_inc
due to the application of deposit fees, but does not have to be lower.credit_inc
is provided here because the user cannot reliably compute it himself from other data.The
credit
field is the absolute credit balance after any newly detected deposit has been credited.If multiple deposit transactions happened concurrently with calls to
notify
then the end result may depend on timing. For example, say the ledger fee is 10 and the initial credit balance of the user is 0. If a deposit of 20 tokens is made, thennotify
is called, then another 20 tokens are deposited andnotify
is called again then the twonotify
responses are:{ deposit_inc = 20; credit_inc = 10; credit = 10 }
,{ deposit_inc = 20; credit_inc = 10; credit = 20 }
. If the firstnotify
arrives after the second deposit then two responses are:{ deposit_inc = 40; credit_inc = 30; credit = 30 }
,{ deposit_inc = 0; credit_inc = 0; credit = 30 }
. In this case the deposit fee is applied only once because the service sees it as one deposit.The service is free to expand the response record with additional optional fields. For example, if the service has expanded the argument record with a field specifying an action which is done after the notification then it may want to also expand the response record with a field describing the result of that action.
Tracked balance
It was said above that
deposit_inc
returned bynotify
is the difference in deposit balance relative to the last known (= "tracked") deposit balance. The tracked deposit balance can be queried with the following method.If the specified
Token
is not supported by the service then the call will throw the async errorcanister_reject
with error message"UnknownToken"
.Otherwise the method the returns the following type.
The
Amount
returned is the currently known balance that the caller has in the specifiedToken
.For example, say a deposit flow has been interrupted during the notification step. The user does not know if the attempted call to
notify
has gone through or not. Then the user can query the ledger to obtain the balance in the deposit account and can query the service to obtain the known deposit balance. If they differ then the user must callnotify
again.Of course, the user can call
notify
directly but the two query calls are considered cheaper and faster. Hence this query method is provided.If any concurrent downstream calls to the ledger are underway that could affect the returned
Amount
then the service returns theErr = NotAvailable
variant. This indicates to the user to try again later. For example, the downstream call could be a balance query (triggered bynotify
) or a consolidation transfer that relates to the caller's deposit account for the specifiedToken
.Deposit
An alternative way to make deposits is via allowances. The user has to set up an allowance for one of its subaccounts with the service's principal as the spender. The user then calls the function
with the following argument:
token
is the Token that is being deposited.amount
is the amount that is to be drawn from the allowance into the service. Any ledger transfer fees will be added on the user account's side.subaccount
is the user's subaccount that carries the allowance wherenull
means the default account.If successful, the call returns:
Possible errors that can occur are:
Withdrawal
The user can initiate a withdrawal with the following method.
with
The
WithdrawArgs
record specifies theToken
to be withdrawn, the destination account and theAmount
to be taken from the caller's credits.If the specified
Token
is not supported by the service then the call will throw the async errorcanister_reject
with error message"UnknownToken"
.If the specified
Subaccount
is not 32 bytes long then the call will throw the async errorcanister_reject
with error message"InvalidSubaccount"
.Otherwise, the following result type is returned.
If the user's credit is below the requested
Amount
thenErr = InsufficientCredit
is returned.If the requested
Amount
is smaller than the Token parametermin_withdrawal
thenErr = AmountBelowMinimum
is returned.If the downstream call to the ICRC-1 ledger fails with an async error then
Err = CallLedgerError
is returned. The accompanying text message should indicate the actual async error that happened.Otherwise the
Ok
variant is returned. It contains thetxid
on the underlying ICRC-1 ledger of the withdrawal transfer. It contains theAmount
that was actually received by the user. In general, thisAmount
will differ from the requested amount becausewithdrawal_fee
was deducted.FAQ
Why is
notify
access-controlled?Notify is not idempotent in its return value. If someone else can call notify for us then we could miss an incremental value.
Notify calls are expensive for the service because of the downstream inter-canister call that they trigger. Restricting the caller makes it easier to control or charge for that cost.
Why is the credit balance access-controlled?
Deposits are publicly visible on the ICRC-1 ledger. Any observer can conclude from those deposit transactions to corresponding incoming credits for the user. But from there on further changes to the credit balance, increase or decrease, depend on the usage of the service by the user. For example, in a DEX the credit changes would correspond to bids placed or trades executed. We do not want to leak that information.
Why does
notify
use a balance-based approach, not transaction-based?The transaction-based approach would mean that the user "claims" a specific deposit transaction where the transaction is specified by txid and is bound to the user by memo. The advantage is that individual deposit accounts can be avoided, hence the consolidation step is not needed which saves fees.
The disadvantages are:
We prefer the approach that requires less state. It makes the service leaner and easier to handle upgrades.
What are the benfits of using
notify
vs allowances?Allowances are simpler to process for the service. Overall transaction fees are lower if an allowance is used for multiple deposits.
But allowances do not always work, for example if