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

Allow viewing contents of satscard #4176

Merged
merged 30 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ rss = "2.0.1"
rust-embed = "8.0.0"
rustls = { version = "0.23.20", features = ["ring"] }
rustls-acme = { version = "0.12.1", features = ["axum"] }
secp256k1 = { version = "*", features = ["global-context"] }
serde-hex = "0.1.0"
serde.workspace = true
serde_json.workspace = true
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Summary
- [Examples](inscriptions/examples.md)
- [Runes](runes.md)
- [Specification](runes/specification.md)
- [Satscard](satscard.md)
- [FAQ](faq.md)
- [Contributing](contributing.md)
- [Donate](donate.md)
Expand Down
65 changes: 65 additions & 0 deletions docs/src/satscard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Satscard
========

[Satscards](https://satscard.com/) are cards which can be used to store
bitcoin, inscriptions, and runes.

Slots
-----

Each satscard has ten slots containing private keys with corresponding bitcoin
addresses.

Initially, all slots are sealed and the private keys are stored only the
satscard.

Slots can be unsealed, which allows the corresponding private key to be
extracted.

Unsealing is permanent. If a satscard is sealed, you can have some confidence
that private key is not known to anyone. That taking physical ownership of a
satscard makes you the sole owner of assets in any sealed slots.

Lifespan
--------

Satscards are expected to have a usable lifetime of ten years. Do not use
satscards for long-term storage of valuable assets.


Viewing
-------

When placed on a smartphone, the satscard transmits a URL, beginning with
`https://satscard.com/start` or `https://getsatscard.com/start`, depending on
when it was manufactured.

This URL contains a signature which can be used to recover the address of the
current slot. This signature is made over a random nonce, so it changes every
time the satscard is tapped, and provides some confidence that the satscard
contains the private key.

`ord` supports viewing the contents of a satscard by entering the full URL into
the `ord` explorer search bar, or the input field on the `/satscard` page.

For `ordinals.com`, this is
[ordinals.com/satscard](https://ordinals.com/satscard).

Unsealing
---------

Satscard slots can be unsealed and the private keys extracted using the `cktap`
binary, available in the
[coinkite-tap-proto](https://github.com/coinkite/coinkite-tap-proto)
repository.

Sweeping
--------

After a satscard slot is unsealed, all assets should be swept from that slot to
another wallet, as the private key can now be read via NFC.

`ord` does not yet support sweeping assets from other wallets, so assets will
need to be transferred manually.

Be careful, and good luck!
8 changes: 8 additions & 0 deletions src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ impl Chain {
self.into()
}

pub(crate) fn bech32_hrp(self) -> KnownHrp {
match self {
Self::Mainnet => KnownHrp::Mainnet,
Self::Regtest => KnownHrp::Regtest,
Self::Signet | Self::Testnet | Self::Testnet4 => KnownHrp::Testnets,
}
}

pub(crate) fn default_rpc_port(self) -> u16 {
match self {
Self::Mainnet => 8332,
Expand Down
1 change: 1 addition & 0 deletions src/deserialize_from_str.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::*;

#[derive(Debug)]
pub struct DeserializeFromStr<T: FromStr>(pub T);

impl<'de, T: FromStr> Deserialize<'de> for DeserializeFromStr<T>
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl From<Error> for SnafuError {
/// We currently use `anyhow` for error handling but are migrating to typed
/// errors using `snafu`. This trait exists to provide access to
/// `snafu::ResultExt::{context, with_context}`, which are otherwise shadowed
/// by `anhow::Context::{context, with_context}`. Once the migration is
/// by `anyhow::Context::{context, with_context}`. Once the migration is
/// complete, this trait can be deleted, and `snafu::ResultExt` used directly.
pub(crate) trait ResultExt<T, E>: Sized {
fn snafu_context<C, E2>(self, context: C) -> Result<T, E2>
Expand Down
5 changes: 5 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,11 @@ impl Index {
})
}

#[cfg(test)]
pub(crate) fn chain(&self) -> Chain {
self.settings.chain()
}

pub fn have_full_utxo_index(&self) -> bool {
self.first_index_height == 0
}
Expand Down
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ use {
teleburn, ParsedEnvelope,
},
into_usize::IntoUsize,
option_ext::OptionExt,
outgoing::Outgoing,
representation::Representation,
satscard::Satscard,
settings::Settings,
signer::Signer,
subcommand::{OutputFormat, Subcommand, SubcommandResult},
Expand All @@ -43,10 +45,10 @@ use {
hash_types::{BlockHash, TxMerkleNode},
hashes::Hash,
policy::MAX_STANDARD_TX_WEIGHT,
script,
script, secp256k1,
transaction::Version,
Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxIn,
TxOut, Txid, Witness,
Amount, Block, KnownHrp, Network, OutPoint, Script, ScriptBuf, Sequence, SignedAmount,
Transaction, TxIn, TxOut, Txid, Witness,
},
bitcoincore_rpc::{Client, RpcApi},
chrono::{DateTime, TimeZone, Utc},
Expand Down Expand Up @@ -119,11 +121,13 @@ mod inscriptions;
mod into_usize;
mod macros;
mod object;
mod option_ext;
pub mod options;
pub mod outgoing;
mod re;
mod representation;
pub mod runes;
mod satscard;
pub mod settings;
mod signer;
pub mod subcommand;
Expand Down
39 changes: 39 additions & 0 deletions src/option_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use super::*;

/// We currently use `anyhow` for error handling but are migrating to typed
/// errors using `snafu`. This trait exists to provide access to
/// `snafu::OptionExt::{context, with_context}`, which are otherwise shadowed
/// by `anyhow::Context::{context, with_context}`. Once the migration is
/// complete, this trait can be deleted, and `snafu::OptionExt` used directly.
pub trait OptionExt<T>: Sized {
fn snafu_context<C, E>(self, context: C) -> Result<T, E>
where
C: snafu::IntoError<E, Source = snafu::NoneError>,
E: std::error::Error + snafu::ErrorCompat;

#[allow(unused)]
fn with_snafu_context<F, C, E>(self, context: F) -> Result<T, E>
where
F: FnOnce() -> C,
C: snafu::IntoError<E, Source = snafu::NoneError>,
E: std::error::Error + snafu::ErrorCompat;
}

impl<T> OptionExt<T> for Option<T> {
fn snafu_context<C, E>(self, context: C) -> Result<T, E>
where
C: snafu::IntoError<E, Source = snafu::NoneError>,
E: std::error::Error + snafu::ErrorCompat,
{
snafu::OptionExt::context(self, context)
}

fn with_snafu_context<F, C, E>(self, context: F) -> Result<T, E>
where
F: FnOnce() -> C,
C: snafu::IntoError<E, Source = snafu::NoneError>,
E: std::error::Error + snafu::ErrorCompat,
{
snafu::OptionExt::with_context(self, context)
}
}
2 changes: 2 additions & 0 deletions src/re.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ lazy_static! {
pub(crate) static ref RUNE_ID: Regex = re(r"[0-9]+:[0-9]+");
pub(crate) static ref RUNE_NUMBER: Regex = re(r"-?[0-9]+");
pub(crate) static ref SATPOINT: Regex = re(r"[[:xdigit:]]{64}:\d+:\d+");
pub(crate) static ref SATSCARD_URL: Regex =
re(r"https://(get)?satscard.com/start#(?<parameters>.*)");
pub(crate) static ref SAT_NAME: Regex = re(r"[a-z]{1,11}");
pub(crate) static ref SPACED_RUNE: Regex = re(r"[A-Z•.]+");
}
Expand Down
Loading
Loading