Skip to content
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

Add Wallet list_unspent method #158

Merged
merged 4 commits into from
Jul 12, 2022

Conversation

zoedberg
Copy link
Contributor

@zoedberg zoedberg commented Jun 9, 2022

Here is my attempt at implementing the list_unspent method.

In order to test this I've updated the bdk-ffi submodule in the bdk-kotlin project, ran commands to build and publish the android library to a local maven repository and finally imported the library on an Android project written in Kotlin.

The list seems to work fine, except for a strange behaviour I've noticed when calling the method multiple times.

So, in order to exclude bugs from my side, I've wrote a minimal Rust project that directly uses bdk 0.18 to list the unspents of an existing wallet.

From this test I've understood that creating a wallet using a bdk::database::SqliteDatabase database and calling the wallet sync method leads to each UTXO getting re-inserted in the database even if already present.

Here's a minimal piece of code that shows how to reproduce the bug:

let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
let database = bdk::database::SqliteDatabase::new("./testdb.sqlite".to_string());
let wallet = Wallet::new(
    "wpkh(tprv8ZgxMBicQKsPdqrqwQmJsdw7Sbt36hJPJyoCQ5BhFwfLcfVHQEwiwPykQPLT9AxUYS26BPQ4yrqTYYGG4HPg4UwJzLJCnK5L8YC6VAUb8nZ/84'/1'/0'/0/*)",
    Some("wpkh(tprv8ZgxMBicQKsPdqrqwQmJsdw7Sbt36hJPJyoCQ5BhFwfLcfVHQEwiwPykQPLT9AxUYS26BPQ4yrqTYYGG4HPg4UwJzLJCnK5L8YC6VAUb8nZ/84'/1'/0'/1/*)"),
    Network::Testnet,
    database,
)?;

wallet.sync(&blockchain, SyncOptions::default())?;

println!("Num unspents: {}", wallet.list_unspent()?.len());

>> Num unspents: 1

wallet.sync(&blockchain, SyncOptions::default())?;

println!("Num unspents: {}", wallet.list_unspent()?.len());

>> Num unspents: 2

println!("Num unspents: {}", wallet.list_unspent()?.len());

>> Num unspents: 2

wallet.sync(&blockchain, SyncOptions::default())?;

println!("Num unspents: {}", wallet.list_unspent()?.len());

>> Num unspents: 3

println!("Num unspents: {}", wallet.list_unspent()?.len());

>> Num unspents: 3

Inspecting the sqlite database shows that the UTXO actually gets re-inserted at each sync.

The error does not arise when the database is defined as MemoryDatabase::default().

@thunderbiscuit
Copy link
Member

Hey thanks for your contribution!

I think the bug you're uncovering is described in #155 and will be fixed once we use bdk 0.19.0 (should be released in the next few days!). So that should be taken care of at least.

Once that's done we'll try and release a new version of bdk-ffi right away. Not sure if we'll try and add new features in or just bump to bdk 0.19.0, which is a fairly big upgrade in itself (@notmandatory what do you think?), but in any case I'm happy to take a look at this PR.

@notmandatory
Copy link
Member

This looks like great stuff. As for the next bdk-ffi release the focus will be on upgrading to the bdk 0.19.0 release. But once that's out we can get back to adding new APIs like this one.

}
}

pub struct LocalUtxo {
Copy link
Member

@thunderbiscuit thunderbiscuit Jun 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BDK version of this struct has a boolean is_spent. I assume you didn't include it here because LocalUtxo will only be returned from the list_unspent() method, which means that boolean will only ever been set to false. In this case, however, I think the naming could lead to confusion if some developers are familiar with the Rust version of BDK.

I propose we either:

  1. Rename the struct UnspentLocalUtxo to make explicit the fact that it is unspent, or
  2. Add the is_spent boolean to it, which for now will only ever be false

I suggest we go with (2) because to me that feels more forward-compatible. If ever we implement other features that use the LocalUtxo struct we'll have it in its entirety already. I also feel like it might allow you to simplify some of the mapping between the LocalUtxo struct you have here and the one defined in BDK. Finally, we've been discussing how we want to try and keep the API in the bindings as close as we can to the BDK API. It's not always possible, but in this case I think it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also feel that (2) is a better choice here so just added the field

src/lib.rs Outdated
address: String,
}

pub enum KeychainKind {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need the 0 and 1 here but maybe there is something I'm missing. Can you tell me why you added them and not just use the enum defined in BDK directly? (i.e. no need to define it here in Rust and you could simply have it in the UDL file I think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: my tests pass when refactoring the KeychainKind out of the lib.rs file like so:

// pub enum KeychainKind {
//     External = 0,
//     Internal = 1,
// }

// impl From<bdk::KeychainKind> for KeychainKind {
//     fn from(x: bdk::KeychainKind) -> KeychainKind {
//         match x {
//             bdk::KeychainKind::External => KeychainKind::External,
//             bdk::KeychainKind::Internal => KeychainKind::Internal,
//         }
//     }
// }

impl NetworkLocalUtxo for LocalUtxo {
    fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo {
        LocalUtxo {
            outpoint: OutPoint {
                txid: x.outpoint.txid.to_string(),
                vout: x.outpoint.vout,
            },
            txout: TxOut {
                value: x.txout.value,
                address: bdk::bitcoin::util::address::Address::from_script(
                    &x.txout.script_pubkey, network).unwrap().to_string(),
            },
            keychain: x.keychain,
        }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my mistake. I initially assumed that was a requirement of uniFFI but then forgot to try without. Thanks for the suggestion!


pub struct TxOut {
value: u64,
address: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance it looks good to me, but it's still something I want to bring to the attention of other reviewers (@notmandatory, @artfuldev). The TxOut in BDK contains a Script instead of an address of type String. I don't think we need the Script at the moment, but it does change the shape of the API, so something to note.

Copy link
Member

@thunderbiscuit thunderbiscuit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a bunch for tackling this @zoedberg! Coin selection is an important feature and this is the first step.

I left a few comments there for you; let me know if they all make sense.

src/lib.rs Outdated
Comment on lines 205 to 199
trait NetworkLocalUtxo: {
fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo;
}

impl NetworkLocalUtxo for LocalUtxo {
fn from_utxo(x: &bdk::LocalUtxo, network: Network) -> LocalUtxo {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love it if we started commenting our codebase more (the FFI layer can be quite opaque and I want to make sure we make it easy on new contributors), and I think this is a perfect place to start.

It took me a few minutes to understand what this NetworkLocalUtxo trait was for, and if I understand correctly, you added this trait because the TxOut struct now contains an address (String), and for that you need the Address::from_script() which requires a network. That's great stuff and let me know if I didn't understand correctly. Long-winded way to say... I think we should just add a small comment explaining what this NetworkLocalUtxo is used for 🤣.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some hard times finding a fitting and self-explanatory name for this trait, so I agree a comment could be useful here. For next contributions I will keep in mind that comments are welcome 😄

@zoedberg
Copy link
Contributor Author

@thunderbiscuit A big thanks to you and everybody who participated to this project, it's a pleasure to contribute to such a well designed and written project. I agree with all the comments, working on in it right now, will push the changes soon :)

@zoedberg
Copy link
Contributor Author

Rebased the PR branch and added the suggested changes. Tested them on Android where they seem to work fine :)

@thunderbiscuit
Copy link
Member

This is ready for merge IMO. I'll wait for one more reviewer to chime in and will squash your commits into one + merge.

@thunderbiscuit
Copy link
Member

@notmandatory would love your review on this. I'm not sure if we should merge before #154, as this is an easier and ready to go change, 154 might be still WIP a bit (but maybe not? happy to do it the other way too). In any case merging this will allow us to start reviewing #164, which I'd love to start working on this week.

Copy link
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK fad5e3c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

3 participants