Integrating with the Internet Computer ledger

Thank you so very much!

Hi, were you able to fix this?

has the documentation for interacting with the ledger canister via a standard message from another canister been updated and released yet?

Good question, let me ping the team and find out.

Hi Jesse, not yet – we’re in the process of doing this

1 Like

ok. do you have an approximate date when the documentation might be released?

We develop the documentation in conjunction with the new Candid interface, which in turn is somewhat aligned with the “Canisters can transfer ICP” feature. This makes it a bit hard to estimate, but I would say 2 - 3 weeks.

2 Likes

Is the create-canister functions of the ledger going to be a part of the new public candid interface? is it going to change, and if it will change, will there be the documentation for it?

The ledger itself does not have a create-canister function, per se. Perhaps you may be referring to the method notify() which can be used to create canisters. Our plans are to modify/replace that method so I think that this replacement will not be immediately (if at all) available as part of the candid interface.

Got it, thanks. Yes I meant the functionality. Can it be public what the plans are for the change?

just following up on this. I was looking for the documentation of interacting with the ledger canister via canister calls, and wasn’t quite able to find anything.

Hi @Jesse!

Here is the current public interface of the Ledger canister: ic/ledger.did at 09af55f2e9975aa19ea8fb5136d8b245d6d0e003 · dfinity/ic · GitHub

Ledger canister id: ryjl3-tyaaa-aaaaa-aaaba-cai

Instructions how to setup ledger locally for testing: ic/rs/rosetta-api/ledger_canister at master · dfinity/ic · GitHub

If you use Rust, you might find this library helpful: ic_ledger_types - Rust

Examples of canisters in Rust and Motoko talking to the Ledger:

Community conversation talk explaining how you could implement payments on top of transfers:

Please let me know if you have questions that aren’t answered by this documentation!

8 Likes

Thank you! I’ve taken some time to look over the examples you gave me. I have one question so far, how do i get a canister to be able to receive ICP? is it as simple as going into my wallet and sending ICP to the canister’s wallet address? if so, how do i get the canister’s wallet address?

I’m learning this now! It’s a little non-trivial, although it is covered in the examples Roman provided.

You can get an account-identifier by hashing the principal of the canister together with a “subaccount” which can be pretty much any other value converted into a blob. Any account-identifier that starts with your canister’s principal can be controlled by your canister.

You’ll need to convert it into a string, for people to send money to it, though.

Because this isn’t very easy to drop into an existing app, I’m building an invoice canister that will simplify the experience for a lot of use cases. Payments - Invoice Canister Design Review. With the invoice, you’ll be able to run a simple query to find out your default account-identifier to transfer into, and you’ll be able to make an invoice for an amount to be paid, and easily check whether the payment has been satisfied

4 Likes

Yep, transfer to a canister is regular ledger transfer.

I’d say the easiest way to figure out the canister account identifier is to add a query endpoint that returns it: examples/main.mo at 85e31a49a8f9ac095ac8eda4a1dc3613d4a4e77d · dfinity/examples · GitHub

You convert the binary AccountIdentifier to hex, this will be the ledger account id that you can use on the mainnet with the tool of your choice (dfx ledger transfer, NNS Dapp, Coinbase, etc.).

Unfortunately, the workflow for local ledger deployments is a bit more complicated and needs a direct call to the local ledger canister, something like

dfx canister call ledger transfer '(record { to = blob "\08.\cf.?dz\c6\00\f4?8\a6\83B\fb\a5\b8\e6\8b\08_\02Y+w\f3\98\08\a8\d2\b5"; memo = 1; amount = record { e8s = 2_00_000_000 }; fee = record { e8s = 10_000 }; })'

Where to is the binary account id that your canister reports.

It seems that we need the following:

  1. Extend dfx to include tools that allow you to compute canister ledger account identifier without writing any code (Extend `dfx ledger account-id` to support canister accounts · Issue #1981 · dfinity/sdk · GitHub)
  2. Document how to do transfer ICP to a canister given canister id (As a developer, I want to know how to transfer ICP to my canister so that I can top up my canister · Issue #655 · dfinity/docs · GitHub)
  3. (Long term) extend dfx to allow everyone to easily install ledger in a way that integrates well with dfx ledger commands (Make installing ledger locally simple · Issue #1982 · dfinity/sdk · GitHub).

The first two points are simple, I’ll try to implement them in the next couple of weeks. The third point is a bit more involved and will take longer.

5 Likes

in the accountIdentifier function within the Account.mo file of the example you gave, as I understand it, you perform the following:
1.) first, you hash an array consisting of a single entry. that entry is 0x0A.
2.) next, you hash the text “account-id”.
3.) next, you hash the user’s principal.
4.) next, you hash the user’s subaccount.
5.) then you concatenate those hashes together to get the hashsum.
6.) then you get the CRC32 checksum of the hashsum.
7.) then you run the checksum though the beBytes function.
8.)finally, you append the array returned from running the checksum through the beBytes function with the hashsum array.

My first question is, why’d you have to hash that first array with the 0x0A entry? whats the significance of that?

my second question is, why’d you have to run the checksum through the beBytes function and append the result to the hashsum array?

1 Like

I did a bit more researching. I think i may know the answer to my first question. Still clueless about the second question though.

Initially, i was wondering if that specific byte (0x0A) had some sort of significance in particular, but, now I’m guessing the answer to why you hashed the first array with the single entry of 0x0A was just to add an extra layer of hashing to minimize the probability of someone being able to decrypt the real data you intend to hash which are the text (‘account-id’), the principal and the subaccount. 0x0A is just an arbitrary byte that was chosen, it could have been any byte. is that correct?

1 Like

why’d you have to hash that first array with the 0x0A entry? whats the significance of that?

0x0A is 10, which is the length of the string “account-id” that follows. This technique of prefixing values being hashed with an identifier is called domain separators. The IC uses domain separators almost every time when something needs to be hashed (search for “domain separator” in The Internet Computer Interface Specification :: Internet Computer for more examples).

why’d you have to run the checksum through the beBytes function and append the result to the hashsum array?

We prepend the CRC32 sum of the hash to form an account identifier as an additional safety measure. It’s a bit like the Luhn check for credit card numbers. The price of a mistake is high: someone might select a wrong hex string that happens to have the right length and lose ICP by transferring to that “account”. That actually happened before with the Protobuf interface that accepts 28-byte identifiers (these don’t have the checksum).
The beBytes function converts the checksum, which is a 32-bit unsigned integer, into a byte array. There is more than one way to do that, and beBytes stands for “big-endian bytes” (see Endianness - Wikipedia for more information on this).

The Ledger Canister Specification also has a section describing the formation of account identifiers.

4 Likes

Thanks a ton! this was enlightening.

The issue is that on Ethereum a transferAndCall method would be atomic… Imagine a user is trying to pay a dapp canister tokens for some service. Then, the user would call the transferAndCall method on the token ledger canister, which would then call some tokenFallback method on the dapp canister. If any part of this fails, the entire transaction is reverted.

But on the IC, inter-canister calls are not atomic… so porting transferAndCall over to the IC as is doesn’t really make sense since it can’t be treated as atomic anyways. But then, why create a new notify method on the ICP ledger canister? It still makes an inter-canister call to the destination canister…

In general, one thing that bothers me is that we jump through all these hoops to avoid making inter-canister calls due to safety concerns with upgrades, atomicity, etc… But inter-canister calls are an essential ingredient in the IC’s vision of composability. I hope they can be made safer and more ergonomic so that developers can be encouraged to use them.

2 Likes