Latest ICP ledger for local testing

I am following the instructions here: ic/rs/rosetta-api/ledger_canister at master · dfinity/ic · GitHub and the Candid files I am getting seem pretty out of date with the current master branch.

I’ve changed the commit hash to the latest commit and I get authorization errors when requesting the Candid files.

Does anyone know how to test on the latest ICP ledger locally?

I assume if I get the correct latest hash things will work, but I don’t see releases on the GitHub page. How do I find the commit hash that will work locally with the latest changes to the ledger?

Also I am confused with the two Candid files, which one are we supposed to use when? It looks like on an initial deploy you would use the “public” one, but the “private” one seems to be used be people as well and still exposed publicly. Also the latest public/private version seem somewhat swapped with the ones from the example.

I looked through recent proposals to try and find a new replica version, and I found this proposal: Internet Computer Network Status

The hash specified seems to have worked. And I suppose I got the public/private candid files mixed up, the private one is just for deploying it seems. It just seemed strange that the public one requires blobs for account identifiers, I would have assumed the public interface would make that easier.

I was mixed up.

This has caused me a massive headache over the last couple of weeks. They did warn us that it was a temporary API and it makes sense to send accounts as binary instead of text when going inner canister makes sense. If we had to type binary into DFX it would not make things better. Things could be clearer for sure.

I’ve combined them below(may be some extra functions fro the ogy ledger…but you can remove those):



type CanisterId = principal;

type HeaderField = record {text; text};
type HttpRequest = record {
  url: text;
  method: text;
  body: vec nat8;
  headers: vec HeaderField;
};

type HttpResponse = record {
  body: vec nat8;
  headers: vec HeaderField;
  status_code: nat16;
};

//check
type TipOfChainRes = record {
    certification: opt vec nat8;
    tip_index: BlockHeight;
};

//check
type Hash = opt record {
  inner: blob;
};

//check
type BlockArg = BlockHeight;

//check
//maybe needs to be BlockDFX
type BlockResDFX = opt variant {
    Ok: opt variant {
        Ok: BlockDFX;
        Err: CanisterId;
    };
    Err: text;
};


//----------------------------------------------
//check
type Duration = record {
    secs: nat64;
    nanos: nat32;
};

//check
type ArchiveOptions = record {
    trigger_threshold : nat64;
    num_blocks_to_archive : nat64;
    node_max_memory_size_bytes: opt nat64;
    max_message_size_bytes: opt nat64;
    controller_id: principal;
    cycles_for_archive_creation: opt nat64;
};

// Height of a ledger block.
//check
type BlockHeight = nat64;




// Arguments for the `send_dfx` call.
//check
type SendArgs = record {
    memo: Memo;
    amount: Tokens;
    fee: Tokens;
    from_subaccount: opt SubAccount;
    to: AccountIdentifierDFX;
    created_at_time: opt TimeStamp;
};


// Arguments for the `notify` call.
//check
type NotifyCanisterArgs = record {
    // The of the block to send a notification about.
    block_height: BlockHeight;
    // Max fee, should be 10000 e8s.
    max_fee: Tokens;
    // Subaccount the payment came from.
    from_subaccount: opt SubAccount;
    // Canister that received the payment.
    to_canister: principal;
    // Subaccount that received the payment.
    to_subaccount: opt SubAccount;
};


//check
type LedgerCanisterInitPayload = record {
    minting_account: AccountIdentifierDFX;
    initial_values: vec record {AccountIdentifierDFX; Tokens};
    max_message_size_bytes: opt nat64;
    transaction_window: opt Duration;
    archive_options: opt ArchiveOptions;
    send_whitelist: vec principal;
    standard_whitelist: vec principal;
    transfer_fee: opt Tokens;
    token_symbol: opt text;
    token_name: opt text;
    admin: principal;
};

//-------------------
//-------------------
//check
type Tokens = record {
     e8s : nat64;
};

// Number of nanoseconds from the UNIX epoch in UTC timezone.
//check
type TimeStamp = record {
    timestamp_nanos: nat64;
};

// AccountIdentifier is a 32-byte array.
// The first 4 bytes is big-endian encoding of a CRC32 checksum of the last 28 bytes.
//problem = is this vec nat8 or text
type AccountIdentifierDFX = text;

type AccountIdentifier = blob;

// Subaccount is an arbitrary 32-byte byte array.
// Ledger uses subaccounts to compute the source address, which enables one
// principal to control multiple ledger accounts.
//check - problem - used to be vec nat8
type SubAccount = blob;

// Sequence number of a block produced by the ledger.
//check
type BlockIndex = nat64;

// An arbitrary number associated with a transaction.
// The caller can set it in a `transfer` call as a correlation identifier.
//check
type Memo = nat64;

// Arguments for the `transfer` call.
//check
type TransferArgs = record {
    // Transaction memo.
    // See comments for the `Memo` type.
    memo: Memo;
    // The amount that the caller wants to transfer to the destination address.
    amount: Tokens;
    // The amount that the caller pays for the transaction.
    // Must be 10000 e8s.
    fee: Tokens;
    // The subaccount from which the caller wants to transfer funds.
    // If null, the ledger uses the default (all zeros) subaccount to compute the source address.
    // See comments for the `SubAccount` type.
    from_subaccount: opt SubAccount;
    // The destination account.
    // If the transfer is successful, the balance of this address increases by `amount`.
    to: AccountIdentifier;
    // The point in time when the caller created this request.
    // If null, the ledger uses current IC time as the timestamp.
    created_at_time: opt TimeStamp;
};

// Arguments for the `transfer_standard_stdldg` call.
//check
type TransferStandardArgs = record {
    // Transaction memo.
    // See comments for the `Memo` type.
    memo: Memo;
    // The amount that the caller wants to transfer to the destination address.
    amount: Tokens;
    // The amount that the caller pays for the transaction.
    // Must be 10000 e8s.
    fee: Tokens;
    // The principal from which the standard canister wants to transfer funds.
    
    from_principal: principal;
    // The subaccount from which the caller wants to transfer funds.
    // If null, the ledger uses the default (all zeros) subaccount to compute the source address.
    // See comments for the `SubAccount` type.
    from_subaccount: opt SubAccount;
    // The destination account.
    // If the transfer is successful, the balance of this address increases by `amount`.
    to: AccountIdentifier;
    // The point in time when the caller created this request.
    // If null, the ledger uses current IC time as the timestamp.
    created_at_time: opt TimeStamp;
};

//check
type TransferError = variant {
    // The fee that the caller specified in the transfer request was not the one that ledger expects.
    // The caller can change the transfer fee to the `expected_fee` and retry the request.
    BadFee : record { expected_fee : Tokens; };
    // The account specified by the caller doesn't have enough funds.
    InsufficientFunds : record { balance: Tokens; };
    // The request is too old.
    // The ledger only accepts requests created within 24 hours window.
    // This is a non-recoverable error.
    TxTooOld : record { allowed_window_nanos: nat64 };
    // The caller specified `created_at_time` that is too far in future.
    // The caller can retry the request later.
    TxCreatedInFuture : null;
    // The ledger has already executed the request.
    // `duplicate_of` field is equal to the index of the block containing the original transaction.
    TxDuplicate : record { duplicate_of: BlockIndex; }
};

//check
type TransferResult = variant {
    Ok : BlockIndex;
    Err : TransferError;
};

// Arguments for the `account_balance_dfx` call.
//check
type AccountBalanceArgsDFX = record {
    account: AccountIdentifierDFX;
};

// Arguments for the `account_balance` call.
//check
type AccountBalanceArgs = record {
    account: AccountIdentifier;
};

//check
type TransferFeeArg = record {};

//check
type TransferFee = record {
    // The fee to pay to perform a transfer
    transfer_fee: Tokens;
};

//check
type GetBlocksArgs = record {
    // The index of the first block to fetch.
    start : BlockIndex;
    // Max number of blocks to fetch.
    length : nat64;
};

//check
type Operation = variant {
    Mint : record {
        to : AccountIdentifier;
        amount : Tokens;
    };
    Burn : record {
        from : AccountIdentifier;
        amount : Tokens;
    };
    Transfer : record {
        from : AccountIdentifier;
        to : AccountIdentifier;
        amount : Tokens;
        fee : Tokens;
    };
};

//check
type OperationBlock = variant {
    Mint : record {
        to : AccountIdentifierDFX;
        amount : Tokens;
    };
    Burn : record {
        from : AccountIdentifierDFX;
        amount : Tokens;
    };
    Transfer : record {
        from : AccountIdentifierDFX;
        to : AccountIdentifierDFX;
        amount : Tokens;
        fee : Tokens;
    };
};

//check
type OperationDFX = variant {
    Burn: record {
        from: AccountIdentifierDFX;
        amount: Tokens;
    };
    Mint: record {
        to: AccountIdentifierDFX;
        amount: Tokens;
    };
    Send: record {
        from: AccountIdentifierDFX;
        to: AccountIdentifierDFX;
        amount: Tokens;
    };
};

//check
type Transaction = record {
    memo : Memo;
    operation : opt Operation;
    created_at_time : TimeStamp;
};

//check
type TransactionDFX = record {
    memo : Memo;
    operation : opt OperationBlock;
    created_at_time : TimeStamp;
};

//check
type Block = record {
    parent_hash : opt blob;
    transaction : Transaction;
    timestamp : TimeStamp;
};

type BlockDFX = record {
    parent_hash : opt blob;
    transaction : TransactionDFX;
    timestamp : TimeStamp;
};

// A prefix of the block range specified in the [GetBlocksArgs] request.
//check
type BlockRange = record {
    // A prefix of the requested block range.
    // The index of the first block is equal to [GetBlocksArgs.from].
    //
    // Note that the number of blocks might be less than the requested
    // [GetBlocksArgs.len] for various reasons, for example:
    //
    // 1. The query might have hit the replica with an outdated state
    //    that doesn't have the full block range yet.
    // 2. The requested range is too large to fit into a single reply.
    //
    // NOTE: the list of blocks can be empty if:
    // 1. [GetBlocksArgs.len] was zero.
    // 2. [GetBlocksArgs.from] was larger than the last block known to the canister.
    blocks : vec Block;
};

// An error indicating that the arguments passed to [QueryArchiveFn] were invalid.
//check
type QueryArchiveError = variant {
    // [GetBlocksArgs.from] argument was smaller than the first block
    // served by the canister that received the request.
    BadFirstBlockIndex : record {
        requested_index : BlockIndex;
        first_valid_index : BlockIndex;
    };

    // Reserved for future use.
    Other : record {
        error_code : nat64;
        error_message : text;
    };
};

//check
type QueryArchiveResult = variant {
    // Successfully fetched zero or more blocks.
    Ok : BlockRange;
    // The [GetBlocksArgs] request was invalid.
    Err : QueryArchiveError;
};

// A function that is used for fetching archived ledger blocks.
//check
type QueryArchiveFn = func (GetBlocksArgs) -> (QueryArchiveResult) query;

// The result of a "query_blocks" call.
//
// The structure of the result is somewhat complicated because the main ledger canister might
// not have all the blocks that the caller requested: One or more "archive" canisters might
// store some of the requested blocks.
//
// Note: as of Q4 2021 when this interface is authored, the IC doesn't support making nested 
// query calls within a query call.
//check
type QueryBlocksResponse = record {
    // The total number of blocks in the chain.
    // If the chain length is positive, the index of the last block is `chain_len - 1`.
    chain_length : nat64;

    // System certificate for the hash of the latest block in the chain.
    // Only present if `query_blocks` is called in a non-replicated query context.
    certificate : opt blob;

    // List of blocks that were available in the ledger when it processed the call.
    //
    // The blocks form a contiguous range, with the first block having index
    // [first_block_index] (see below), and the last block having index
    // [first_block_index] + len(blocks) - 1.
    //
    // The block range can be an arbitrary sub-range of the originally requested range.
    blocks : vec Block;

    // The index of the first block in "blocks".
    // If the blocks vector is empty, the exact value of this field is not specified.
    first_block_index : BlockIndex;

    // Encoding of instructions for fetching archived blocks whose indices fall into the
    // requested range.
    //
    // For each entry `e` in [archived_blocks], `[e.from, e.from + len)` is a sub-range
    // of the originally requested block range.
    archived_blocks : vec record {
        // The index of the first archived block that can be fetched using the callback.
        start : BlockIndex;

        // The number of blocks that can be fetch using the callback.
        length : nat64;

        // The function that should be called to fetch the archived blocks.
        // The range of the blocks accessible using this function is given by [from]
        // and [len] fields above.
        callback : QueryArchiveFn;
    };
};

//check
type Archive = record {
    canister_id: principal;
};

//check
type Archives = record {
    archives: vec Archive;
};

service: (LedgerCanisterInitPayload) -> {
  send_dfx : (SendArgs) -> (BlockHeight);
  notify_dfx: (NotifyCanisterArgs) -> ();
  account_balance_dfx : (AccountBalanceArgsDFX) -> (Tokens) query;
  get_nodes : () -> (vec CanisterId) query;
  http_request: (HttpRequest) -> (HttpResponse) query;

  get_admin: (record {}) -> (principal) query;
  get_send_whitelist_dfx: (record {}) -> (vec principal) query;
  get_minting_account_id_dfx: (record {}) -> (opt AccountIdentifierDFX) query;
  
  set_admin: (principal) -> ();
  set_send_whitelist_dfx: (vec principal) -> ();
  set_minting_account_id_dfx: (AccountIdentifierDFX) -> ();
  
  
  total_supply_dfx : (record {}) -> (Tokens) query;
  tip_of_chain_dfx : (record {}) -> (TipOfChainRes) query;
  transfer : (TransferArgs) -> (TransferResult);

  // Returns the amount of Tokens on the specified account.
  account_balance : (AccountBalanceArgs) -> (Tokens) query;

  // Returns the current transfer_fee.
  transfer_fee : (TransferFeeArg) -> (TransferFee) query;


  // Queries blocks in the specified range.
  query_blocks : (GetBlocksArgs) -> (QueryBlocksResponse) query;

  // Returns token symbol.
  symbol : () -> (record { symbol: text }) query;

  // Returns token name.
  name : () -> (record { name: text }) query;

  // Returns token decimals.
  decimals : () -> (record { decimals: nat32 }) query;

  // Returns the existing archive canisters information.
  archives : () -> (Archives) query;
}

What I did to fetch newest version of the ledger was just going to GitHub - dfinity/ic: Internet Computer blockchain source: the client/replica software run by nodes, clicking commits and copying the newest commit that was available at the time I was pulling the ledger candid files through curl commands mentioned in the ic/rs/rosetta-api/ledger_canister at master · dfinity/ic · GitHub instructions

Also I am confused with the two Candid files, which one are we supposed to use when? It looks like on an initial deploy you would use the “public” one, but the “private” one seems to be used be people as well and still exposed publicly. Also the latest public/private version seem somewhat swapped with the ones from the example.

You first need to deploy the ledger canister locally by using the private file (edit it in the dfx.json to use private. Then, create all the identities (if you don’t have them already as shown in the rosetta-api repository) and after that run the ledger deploy as shown in that repository as well by using
dfx deploy ledger --argument '(record {minting_account = "'${MINT_ACC}'"; initial_values = vec { record { "'${LEDGER_ACC}'"; record { e8s=100_000_000_000 } }; }; send_whitelist = vec {}})'

At this point ledger should be successfully deployed locally (your dfx.json should still have the edger.private.did defined in the config for the ledger canister block. Now, since ledger has been deployed, you can change it back to ledger.public.did and you are good to go! Now you can deploy remaining canisters that your app register by just doing dfx deploy or similar.

So takeaways are: need to first deploy ledger canister on it’s own with dfx deploy ledger (or whatever name you gave your ledger canister in the dfx.json file), then change it to public version in the configuration and deploy remaining canisters for the app.

1 Like

The private interface is the interface of the ledger before the public release. You can find it at rs/rosetta-api/ledger.did. For the public release, Dfinity decided to do a pass over the API to make it simpler and safer without breaking the private interface. That’s when we create the public interface that can be found under rs/rosetta-api/ledger_canister/ledger.did. This is the standard interface of the ledger and it has the following methods:

  transfer : (TransferArgs) -> (TransferResult);
  account_balance : (AccountBalanceArgs) -> (Tokens) query;
  transfer_fee : (TransferFeeArg) -> (TransferFee) query;
  query_blocks : (GetBlocksArgs) -> (QueryBlocksResponse) query;
  symbol : () -> (record { symbol: text }) query;
  name : () -> (record { name: text }) query;
  decimals : () -> (record { decimals: nat32 }) query;
  archives : () -> (Archives) query;

The reason why we kept the private interface is that some users and canisters started using the private interface while we were in the process of defining the public one. We don’t want to break existing canisters so we decided to keep the private interface around.

Account id is a blob inside the ledger and is exposed as blog in the public interface. I think this is the right call as canisters should be able to use blobs with no issues while users can use dfx.

One last thing to mention is that the private interface is still used for ledger initialisation and that’s why it is downloaded in the examples and used at initialisation.

1 Like

Thanks for all of the explanations! I have it working locally. I think the root of my confusion was the latest commit hash not working and seeing the major differences between the Candid files now and in the past, and the fact that the public interface seems more complicated than the private interface, which led me to confuse which one was supposed to be public and which one was supposed to be private.

Basically all sorted now, thanks!

2 Likes