Seed and Wallet
Mix.install([
{:bsv, "~> 2.1"},
{:httpoison, "~> 2.0"},
{:jason, "~> 1.4"},
{:qr_code, "~> 3.0"},
{:kino, "~> 0.14"}
])
network =
case System.get_env("LB_NETWORK", "test") do
"main" -> :main
"test" -> :test
other -> raise ArgumentError, "LB_NETWORK must be \"main\" or \"test\", got: #{inspect(other)}"
end
if network == :main and System.get_env("LIVEWALLET_HOSTED") == "true" do
raise "This is a hosted LiveWallet. Mainnet is only for a Livebook you run yourself."
end
Application.put_env(:bsv, :network, network)
Setup
Generate a seed phrase and turn it into a wallet, so that you hold a key that nothing else can recreate.
Before this: Start here. This is the first notebook on the path; nothing else is required.
network = BSV.network()
woc = "https://api.whatsonchain.com/v1/bsv/#{network}"
{label, hint} =
case network do
:main -> {"MAINNET", "Real bitcoin. Keep amounts under 1 USD."}
:test -> {"TESTNET", "Coins have no value. Addresses start with m or n, not 1."}
end
Kino.Markdown.new("""
> **Network: #{label}** (`#{network}`).
> #{hint}
> To switch, set the Livebook secret `LB_NETWORK` to `main` or `test`, then re-evaluate the setup cell.
""")
Seed words
When using a blockchain, a user signs transactions with keys that are usually managed by wallet software. A wallet can be created from a seed phrase, a set of random words often called "12 words". The BIP39 standard that defines it calls the phrase a mnemonic, and so does the code below.
Your seed phrase is the only thing needed to restore the wallet. Write it down clearly and keep it safe. Holding it yourself makes this wallet non-custodial. A custodial wallet is one where a company holds the keys for you.
flowchart LR
R[random bits] -->|BIP39 wordlist| M[seed phrase]
M -->|PBKDF2| S[seed]
S -->|HMAC-SHA512| X[extended key]
X -->|export| W[WIF]
Run this cell to generate a fresh seed phrase. Change the number of bits for a longer phrase.
# 128 bits gives 12 words. 160, 192, 224 and 256 bits give 15, 18, 21 and 24 words.
generated_mnemonic = BSV.Mnemonic.new(128)
Every time you evaluate the cell above, you get a different seed phrase. To pin one, paste it into the box below. Leave the box blank to use the phrase you just generated.
mnemonic_input =
Kino.Input.textarea("Seed phrase to reuse (leave blank to use the one generated above)")
mnemonic =
case String.trim(Kino.Input.read(mnemonic_input)) do
"" -> generated_mnemonic
pasted -> pasted
end
Create a seed
The words are turned into a seed. The seed is the root from which every key in this wallet grows.
seed = BSV.Mnemonic.to_seed(mnemonic)
From the seed we create an extended key. An extended key can derive further keys, and those can derive more.
In bitcoin, an important privacy consideration is whether to reuse addresses. For better privacy, use a fresh key for each transaction. Computers generate keys cheaply, so there is no reason to be stingy.
extkey = BSV.ExtKey.from_seed!(seed)
BSV.ExtKey.to_string(extkey)
On mainnet the exported extended key starts with xprv.
On testnet it starts with tprv.
This string recreates the extended key, so it must be kept as secret as the seed phrase.
Export the wallet as a WIF
WIF stands for Wallet Import Format. It is a common way to move a single private key between wallets.
wif = BSV.PrivKey.to_wif(extkey.privkey)
Review
You have created a wallet:
- generated a seed phrase
- converted the seed phrase into a seed
- derived an extended key from the seed
- exported the root private key as a WIF
Going forward, you need only the seed phrase. This wallet cannot be recovered without it, and that lesson is specific to cryptography.
Did you write the seed phrase down clearly and permanently?
Kino.Input.checkbox("I have written down my seed phrase")
The cell below checks what you built. It prints PASS, or raises with the failing check.
words = String.split(mnemonic)
prefix = if network == :main, do: "xprv", else: "tprv"
checks = [
{"seed phrase has 12, 15, 18, 21 or 24 words", length(words) in [12, 15, 18, 21, 24]},
{"extended key starts with #{prefix} on #{network}",
String.starts_with?(BSV.ExtKey.to_string(extkey), prefix)},
{"WIF round-trips to the same private key", BSV.PrivKey.from_wif(wif) == {:ok, extkey.privkey}}
]
Enum.each(checks, fn {label, ok?} -> IO.puts("#{if ok?, do: "ok ", else: "FAIL"} #{label}") end)
failed = for {label, false} <- checks, do: label
if failed == [], do: "PASS", else: raise("FAIL: " <> Enum.join(failed, "; "))
Keep for later: your seed phrase (on paper), and nothing else.
Next
Keys and Addresses: derive many keys from this one seed and make one of them reachable.
Stuck or have a question? Open an issue.