Derive addresses from Daedalus wallet public key

Hi everyone!

We have stumbled upon an issue with integrating Cardano into our application. More specifically, we need to derive all addresses from a Daedalus wallet public key. We have been researching this extensively but we haven’t found a solution yet. Does anyone here know how this can be done?

For example, we have the wallet public key: acct_xvk12…zjrzt2
We want to derive all addresses starting with addr1…

This step is necessary to be able to fetch all transactions and balances associated with a Daedalus wallet public key using any of the available API services, such as Blockfrost for example.

Thanks in advance!

That is pretty much infeasible. All addresses would be 2×2^32.

But you only need the ones that have been used. You can get those by https://docs.blockfrost.io/#tag/Cardano-Accounts/paths/~1accounts~1{stake_address}~1addresses/get in Blockfrost or by https://api.koios.rest/#get-/account_addresses in Koios, for example. If you use the usual, delegated addresses that contain the stake address, these will give you all addresses associated with an account. (If you use undelegated, “enterprise” addresses, there is no way except to derive them yourself and try them, not all obviously, but from the beginning until you find a streak of unused addresses.)

Blockfrost as well as Koios use the stake address to identify accounts. If you really only have the account public key, you’d have to derive the stake address from it, first. This can be done, for example, with https://github.com/dcSpark/cardano-multiplatform-lib/blob/develop/doc/getting-started/generating-keys.md or with https://github.com/input-output-hk/cardano-addresses.

1 Like

Note: typically derived public addresses are simply incremented hashes … not sure what the purpose of this would be as it would be much easier to query “used” addresses: API Documentation – Cardano Wallet

Thanks a lot for your help. You are correct that we only need the address actually used, of course :slight_smile: I will send this to our developers and see if we can use this.

Just to confirm that I understood this correctly (I’m not a Cardano expert or developer myself), this is what we need to do:

  • Our users enter their wallet public key (such as acct_xvk12…zjrzt2)
  • We need to derivate the stake address from this public key (such as stake1…)
    → Only one address, correct?
  • From the stake address, we get all the used addresses using the Blockfrost or Koios API for example

Another related question: is this possible also for Yoroi public keys? I see that these addresses do not start with acct_ but are still referred to as “wallet public key”

The link you provided didn’t show the correct page, but I think this is the endpoint you referred to: Cardano Wallet Backend API Documentation

Two questions:

  1. What is {walletId} and how/where can we find this?
  2. Do we need to run a Cardano node to send requests to these endpoints or is there a public API we can use?
1 Like

Yes, there is only one stake address per account.

And I would let the users enter that stake address, not the wallet public key. It is much easier to find and much more consistent across wallet apps.

Eternl exports public keys in the format: xpub1uu3qmkrzf68xpkadf59kartsglq9mv5sqrpcllyd70452kctxq56yze0nfwh6mlp8dvh09a8jw9tncf2mvaqpgyh8eln2mlajunhyfclrf3c3
Yoroi exports public keys in the format: e7220dd8624e8e60dbad4d0b6e8d7047c05db29000c38ffc8df3eb455b0b3029a20b2f9a5d7d6fe13b597797a7938ab9e12adb3a00a0973e7f356ffd97277227
Haven’t found a wallet app that exports acct_xvk1…, but a lot of wallet apps that do not offer public key export at all.
Yoroi’s and Eternl’s formats are both valid, but would have to be converted into one common format for your application.

Just relying on the stake address seems better.

For me, both links go to different but similar explanations of the same API function.

In contrast to services like Blockfrost and Koios, cardano-wallet works with wallets that you have imported to it using the seed phrase. And in that process you get this walletID: https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/postWallet

Does not seem to be a fit for your use case.

Yes, needs to have a cardano-node running. A public API would not make much sense, since secrets are given to this thing by design.

1 Like

I just installed the Daedalus wallet yesterday and the wallet public key starts with acct_. Many of our users have also added their wallet address in this format, so I assume they are using Daedalus if this is the only wallet using this public key format?

That would be easier, but don’t think that is possible with for example Daedalus and Yoroi? If we can derive the stake addresses it shouldn’t be any problem, we just need to implement this extra step.

Not sure if I follow here. Which common format do you mean? In the end, I believe we need to derive the stake address if I understood correctly since this is the only (or easiest) way to get all addresses associated with the account.

Probably. I haven’t found a public key export in Typhon, Flint, and Nami. Eternl uses xpub1 and Yoroi a plain hex string.

But Daedalus users will become less. We get a lot of requests what to do, because Daedalus starts eating too much resources. So, people will switch to light wallets in considerable numbers.

Why not? You can see your stake address in all of these wallet apps, including Daedalus and Yoroi.

For convenience, you could also allow your users to give you any address that they find somewhere in their wallet and extract the stake address from that.

If you request the public key, you should be prepared to accept all three of these formats – acct_xvk1, xpub1, and plain hex from Yoroi. Only accepting one of them will be confusing to users and they will have to find a way to convert themselves.

Sure, you can write stake address derivation for all three of them independently, but that will be largely redundant. So, I meant that internally, you convert, say, all of them to plain hex and then do the stake address derivation from that one to avoid redundancy. That’s more or less an implementation detail.

If you have the public key, querying via the stake address is easiest, but not the only way.

You could derive addresses yourself and individually query them (until you find a streak of, for example, 20 unused addresses). That would result in much more load on the API you are using, but it would also enable to support addresses that are not delegated to a stake address. As far as I know these are only implemented by Eternl and by command line tools, right now. So, they will be very rare.

Exactly, this is what we are trying to achieve! We already have support for stake addresses in our application, so converting the public key to the stake address is the only step remaining.

Agree, this sounds like the best solution. The only thing not clear to me now is how to convert the different public key formats (acct, xpub1) into plain hex as you said. Is there a simple way to do this you know of?

And just to confirm, once we have converted the public key to plain hex (same as Yoroi), we can use one of the methods you mentioned in the first reply to derive the stake address:

Several. Can you tell me, in which ecosystem you develop? Javascript, Python, shell scripts? Then, I could probably give you a better concrete answer.

Our backend code is PHP, so that would be ideal. JavaScript would be the second-best option.

I am not aware of an active PHP library. Calling out to command-line tools would be possible, though.

For Javascript, there are libraries. I’ll get you an example together tonight (European time).

Perfect, looking forward to that!

So, here would be my solution with Javascript (NodeJS) using https://github.com/dcSpark/cardano-multiplatform-lib installed with:

$ npm install @dcspark/cardano-multiplatform-lib-nodejs

Also needs https://github.com/bitcoinjs/bech32 installed by:

$ npm install bech32

get-stake-address.js:

"use strict"

const CML = require("@dcspark/cardano-multiplatform-lib-nodejs/cardano_multiplatform_lib")
const { bech32, bech32m } = require("bech32")
const mainnet = CML.NetworkInfo.mainnet().network_id()

function stake_bech32_to_reward_address(bech32) {
    try {
        const address = CML.Address.from_bech32(bech32)
        const reward_address = CML.RewardAddress.from_address(address)
        return reward_address
    } catch (error) {
        console.log("Given reward address is not valid:", error)
        return null
    }
}

function addr_bech32_to_reward_address(bech32) {
    var base_address = null
    try {
        const address = CML.Address.from_bech32(bech32)
        base_address = CML.BaseAddress.from_address(address)
    } catch (error) {
        console.log("Given base address is not valid:", error)
        return null
    }
    if (base_address) {
        const stake_cred = base_address.stake_cred()
        const reward_address = CML.RewardAddress.new(mainnet, stake_cred)
        return reward_address
    } else {
        console.log("Given base address is not delegated.")
        return null
    }
}

function xpub_bech32_to_account_public_key(bech32) {
    try {
        const key = CML.Bip32PublicKey.from_bech32(bech32)
        return key
    } catch (error) {
        console.log("Given account public key is not valid:", error)
        return null
    }
}

function xpub_hex_to_account_public_key(hex) {
    try {
        const key_bytes = Uint8Array.from(Buffer.from(hex, 'hex'))
        const key = CML.Bip32PublicKey.from_bytes(key_bytes)
        return key
    } catch (error) {
        console.log("Given account public key is not valid:", error)
        return null
    }
}

function account_public_key_to_reward_address(key) {
    const reward_key = key.derive(2).derive(0)
    const hash = reward_key.to_raw_key().hash()
    const stake_cred = CML.StakeCredential.from_keyhash(hash)
    const reward_address = CML.RewardAddress.new(mainnet, stake_cred)
    return reward_address
}

var reward_address = null
const given = process.argv[2]
if (given.startsWith("stake1")) {
    reward_address = stake_bech32_to_reward_address(given)
} else if (given.startsWith("addr1")) {
    reward_address = addr_bech32_to_reward_address(given)
} else if (given.startsWith("xpub1")) {
    const key = xpub_bech32_to_account_public_key(given)
    if (key) {
        reward_address = account_public_key_to_reward_address(key)
    }
} else if (given.startsWith("acct_xvk1")) {
    var translated = null
    try {
        const words = bech32.decode(given, 118).words
        translated = bech32.encode('xpub', words, 114)
    } catch (error) {
        console.log("Given account public key is not valid:", error.message)
    }
    if (translated) {
        const key = xpub_bech32_to_account_public_key(translated)
        if (key) {
            reward_address = account_public_key_to_reward_address(key)
        }
    }
} else {
    const key = xpub_hex_to_account_public_key(given)
    if (key) {
        reward_address = account_public_key_to_reward_address(key)
    }
}

if (reward_address) {
    console.log("Stake address:", reward_address.to_address().to_bech32())
}

Examples:

$ node get-stake-address.js stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
Stake address: stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
$ node get-stake-address.js stake1spamham
Given reward address is not valid: invalid checksum

$ node get-stake-address.js addr1qyl58pw58ph8c0c4mg4ufes8t346xp5z795zp2j4jcdxp32ehzzp5nrmfwgee2ywlyfjhevwuktvc6e420m5wl29w7eq79d5f4
Stake address: stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
$ node get-stake-address.js addr1vyl58pw58ph8c0c4mg4ufes8t346xp5z795zp2j4jcdxp3g0leg3e
Given base address is not delegated.
$ node get-stake-address.js addr1spamham
Given base address is not valid: invalid checksum

$ node get-stake-address.js xpub1egv4345pu90np29wwks3k8pnqvzgdf9egu7vxqwcyef6x3rnkypey64k6e4vr8s5ua9q67rvywc7lavgcjwhj95ed7f3hmn7q3dc5gsvr328c
Stake address: stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
$ node get-stake-address.js xpub1spamham
Given account public key is not valid: Failed to parse bech32, invalid data format

$ node get-stake-address.js acct_xvk1egv4345pu90np29wwks3k8pnqvzgdf9egu7vxqwcyef6x3rnkypey64k6e4vr8s5ua9q67rvywc7lavgcjwhj95ed7f3hmn7q3dc5gsluu2y8
Stake address: stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
$ node get-stake-address.js acct_xvk1spamham
Given account public key is not valid: Invalid checksum for acct_xvk1spamham

$ node get-stake-address.js ca1958d681e15f30a8ae75a11b1c33030486a4b9473cc301d82653a34473b103926ab6d66ac19e14e74a0d786c23b1eff588c49d7916996f931bee7e045b8a22
Stake address: stake1u9vm3pq6f3a5hyvu4z80jyetuk8wt9kvdv648a6804zh0vscalg0n
$ node get-stake-address.js spamham
Given account public key is not valid: Invalid Public Key size

As you can see, the valid examples all lead to the same stake address (an old wallet of mine). You can also see that the acct_xvk1 and the xpub1 public key formats only differ in prefix and checksum, but the part in the middle is identical.

1 Like

Thanks a lot! This is a huge help for us and I will make sure to pass it on to our developers right away so we can start working on this next week.

I will let you know if we run into any issues or when the implementation is completed. Have a great weekend, and thanks again for your help so far!

1 Like