We present here an overview of the basic wallet operations. Language-specific samples are provided in step-by-step guides.
The covered basic operations are:
- Wallet management
- Creating a new multi-coin wallet
- Importing a multi-coin wallet
- Address derivation (receiving)
- Generating the default address for a coin
- Generating an address using a custom derivation path (expert)
- Transaction signing (e.g. for sending)
For the examples we use Bitcoin, Ethereum and Binance Coin as sample coins/blockchains.
Note: Wallet Core does not cover communication with blockchain networks (nodes): address derivation is covered, but address balance retrieval not; transaction signing is covered, but broadcasting transactions to the network not.
In this guide we use small code examples from a Swift sample application, but the focus is on the explanations.
The Multi-Coin Wallet is a structure allowing accounts for many coins, all controlled by a single recovery phrase. It is a standard HD Wallet (Hierarchically Derived), employing the standard derivation schemes, interoperable with many other wallets: BIP39 for recovery phrase, BIP44/BIP84 for account derivation.
When a new wallet is created, a new seed (and thus recovery phrase) is chosen at random.
After creation, the user has to be informed and guided to backup the recovery phrase.
The random generation employs secure random generation, as available on the device.
let wallet = HDWallet(strength: 128, passphrase: "")
Input parameter | Description |
---|---|
strength | The strength of the secret seed. Higher seed means more information content, longer recovery phrase. Default value is 128, but 256 is also possible. |
passphrase | Optional passphrase, used to scramble the seed. If specified, the wallet can be imported and opened only with the passphrase (Not to be confused with recovery phrase). |
A previously created wallet can be imported using the recovery phrase. Typical usecases for import are:
- re-importing a wallet later, into a later installation, or
- importing into another device, or
- importing into another wallet app.
If the wallet was created with a passphrase, it is also required.
let wallet = HDWallet(mnemonic: "ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn fatal", passphrase: "")
Input parameter | Description |
---|---|
mnemonic | a.k.a. recovery phrase. The string of several words that was used to create the wallet. |
passphrase | Optional passphrase, used to encrypt the seed. |
Each coin needs a different account, with matching address. Addresses are derived from the multi-coin wallet.
Derivation is based on a derivation path, which is unique for each coin, but can have other parameters as well.
Each coin has a default derivation path, such as "m/84'/0'/0'/0/0"
for Bitcoin and "m/44'/60'/0'/0/0"
for Ethereum.
The simplest is to get the default address for a coin -- this requires no further inputs. The address is generated using the default derivation path of the coin.
For example, the default BTC address, derived for the wallet with the mnemonic shown above, with the default BTC derivation path (m/84'/0'/0'/0/0
) is:
bc1qpsp72plnsqe6e2dvtsetxtww2cz36ztmfxghpd
.
For Ethereum, this is 0xA3Dcd899C0f3832DFDFed9479a9d828c6A4EB2A7
.
Here is the sample code fort obtaining the default address for different coins:
let addressBTC = wallet.getAddressForCoin(coin: .bitcoin)
let addressETH = wallet.getAddressForCoin(coin: .ethereum)
let addressBNB = wallet.getAddressForCoin(coin: .binance)
It is also possible to derive addresses using custom derivation paths. This can be done in two steps: first a derived private key is obtained, then an address from it.
Warning: use this only if you are well aware of the semantics of the derivation path used!
Security Warning: if secrets such as private keys are handled by the wallet, even if for a short time, handle with care! Avoid any risk of leakage of secrets!
let key = wallet.getKey(derivationPath: "m/44\'/60\'/1\'/0/0") // m/44'/60'/1'/0/0
let address = CoinType.ethereum.deriveAddress(privateKey: key)
For example, a second Ethereum address can be derived using the custom derivation path ”m/44'/60’/1’/0/0”
(note the 1 in the third position),
yielding address 0x68eF4e5660620976a5968c7d7925753D3Cc40809
.
In general, when creating a new blockchain transaction, a wallet has to:
- Put together a transaction with relevant fields (source, target, amount, etc.)
- Sign the transaction, using the account private key. This is done by Wallet Core.
- Send to a node for broadcasting to the blockchain network.
The exact fields needed for a transaction are different for each blockchain. In Wallet Core, signing input and output parameters are typically represented in a protobuf message (internally needed for serialization for passing through different language runtimes).
A generic, coin-independent signer also exists (AnySigner), but its usage is recommended only in browser-based applications.
Bitcoin is the first UTXO
(Unspent Transaction Output) based cryptocurrency / blockchain, if you haven't read the documentation about Bitcoin, we highly recommend you to read developer glossary and raw transaction format, these will help you understand how to sign a Bitcoin transaction. Wallet Core supports Bitcoin, Bitcoin Cash, Zcash, Decred and a few forks.
The most important models in Swift are BitcoinSigningInput
and BitcoinUnspentTransaction
BitcoinSigningInput
Field | Sample value | Description |
---|---|---|
hash_type | BitcoinSigHashType.all | Bitcoin Cash needs to or with TitcoinSigHashType.fork (see Sighash for more details) |
amount | 10000 | Amount (in satoshi) to send (value of new UTXO will be created) |
byteFee | 1 | Transaction fee is byte_fee x transaction_size , Wallet Core will calculate the fee for you by default |
toAddress | bc1q03h6k5lt6pzfjaanz5mlnmuc7aha2t3nkz7gh0 | Recipient address (Wallet Core will build lock script for you) |
changeAddress | 1AC4gh14wwZPULVPCdxUkgqbtPvC92PQPN | Address to receive changes, can be empty if you sweep a wallet |
privateKey | [Data(...), Data(...)] | Private keys for all the input UTXOs in this transaction |
scripts | [script_hash : Data(...)] |
Redeem scripts indexed by script hash, usually for P2SH , P2WPKH or P2WSH |
utxo | [BitcoinUnspentTransaction] | All the input UTXOs, see below table for more details |
useMaxAmount | false | Consume all the input UTXOs, it will affect fee estimation and number of output |
coinType | 145 | SLIP44 Index coin type, default is 0 / Bitcoin |
BitcoinUnspentTransaction
Field | Sample value | Description |
---|---|---|
outPoint | BitcoinOutPoint(hash:index:) | Refer to a particular transaction output, consisting of a 32-byte TXID and a 4-byte output index number (vout) |
amount | 10000 | A value field for transferring zero or more satoshis |
script | 0x76a9146cfa0e96c34fce09c0e4e671fcd43338c14812e588ac | A script (ScriptPubKey) included in outputs which sets the conditions that must be fulfilled for those satoshis to be spent |
Here is the Swift sample code for signing a real world Bitcoin Cash transaction
let utxoTxId = Data(hexString: "050d00e2e18ef13969606f1ceee290d3f49bd940684ce39898159352952b8ce2")! // latest utxo for sender, "txid" field from blockbook utxo api: https://github.com/trezor/blockbook/blob/master/docs/api.md#get-utxo
let privateKey = PrivateKey(data: Data(hexString: "7fdafb9db5bc501f2096e7d13d331dc7a75d9594af3d251313ba8b6200f4e384")!)!
let address = CoinType.bitcoinCash.deriveAddress(privateKey: privateKey)
let utxo = BitcoinUnspentTransaction.with {
$0.outPoint.hash = Data(utxoTxId.reversed()) // reverse of UTXO tx id, Bitcoin internal expects network byte order
$0.outPoint.index = 2 // outpoint index of this this UTXO, "vout" field from blockbook utxo api
$0.outPoint.sequence = UINT32_MAX
$0.amount = 5151 // value of this UTXO, "value" field from blockbook utxo api
$0.script = BitcoinScript.lockScriptForAddress(address: address, coin: .bitcoinCash).data // Build lock script from address or public key hash
}
let input = BitcoinSigningInput.with {
$0.hashType = BitcoinScript.hashTypeForCoin(coinType: .bitcoinCash)
$0.amount = 600
$0.byteFee = 1
$0.toAddress = "1Bp9U1ogV3A14FMvKbRJms7ctyso4Z4Tcx"
$0.changeAddress = "1FQc5LdgGHMHEN9nwkjmz6tWkxhPpxBvBU" // can be same sender address
$0.utxo = [utxo]
$0.privateKey = [privateKey.data]
}
let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoinCash)
guard output.error.isEmpty else { return }
// encoded transaction to broadcast
print(output.encoded)
It's worth to note that you can also calcuate fee and change manually (by using a BitcoinTransactionPlan
struct)
Below is another real world Zcash transparent transaction demonstrate this
let utxos = [
BitcoinUnspentTransaction.with {
$0.outPoint.hash = Data(hexString: "53685b8809efc50dd7d5cb0906b307a1b8aa5157baa5fc1bd6fe2d0344dd193a")!
$0.outPoint.index = 0
$0.outPoint.sequence = UINT32_MAX
$0.amount = 494000
$0.script = Data(hexString: "76a914f84c7f4dd3c3dc311676444fdead6e6d290d50e388ac")!
}
]
let input = BitcoinSigningInput.with {
$0.hashType = BitcoinSigHashType.all.rawValue
$0.amount = 488000
$0.toAddress = "t1QahNjDdibyE4EdYkawUSKBBcVTSqv64CS"
$0.coinType = CoinType.zcash.rawValue
$0.privateKey = [Data(hexString: "a9684f5bebd0e1208aae2e02bc9e9163bd1965ad23d8538644e1df8b99b99559")!]
$0.plan = BitcoinTransactionPlan.with {
$0.amount = 488000
$0.fee = 6000
$0.change = 0
// Sapling branch id
$0.branchID = Data(hexString: "0xbb09b876")!
$0.utxos = utxos
}
}
let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .zcash)
// encoded transaction to broadcast
print(output.encoded)
Besides orignal Bitcoin RPC, there are many other APIs / block explorer can get UTXO and broadcast raw transaction, like: insight api, trezor blockbook, blockchain com, blockchair api.
A simple Ethereum send transaction needs the following fields:
Field | Sample value | Description |
---|---|---|
chainID | 1 | Network selector, use 1 for mainnet (see https://chainid.network for more) |
nonce | 1 | The count of the number of outgoing transactions, starting with 0 |
gasPrice | 3600000000 | The price to determine the amount of ether the transaction will cost |
gasLimit | 21000 | The maximum gas that is allowed to be spent to process the transaction |
to | <address> | The account the transaction is sent to, if empty, the transaction will create a contract |
value | 100000000 | The amount of ether to send |
data | Could be an arbitrary message or function call to a contract or code to create a contract |
Several parameters, like the current nonce and gasPrice values can be obtained from Ethereum node RPC calls (see https://github.com/ethereum/wiki/wiki/JSON-RPC, e.g., eth_gasPrice).
Code example to fill in the signer input parameters:
let input = EthereumSigningInput.with {
$0.chainID = Data(hexString: "01")!
$0.gasPrice = Data(hexString: "d693a400")! // decimal 3600000000
$0.gasLimit = Data(hexString: "5208")! // decimal 21000
$0.toAddress = "0xC37054b3b48C3317082E7ba872d7753D13da4986"
$0.transaction = EthereumTransaction.with {
$0.transfer = EthereumTransaction.Transfer.with {
$0.amount = Data(hexString: "0348bca5a16000")!
}
}
$0.privateKey = wallet.getKeyForCoin(coin: .ethereum).data
}
Then Signer is invoked, and the signed and encoded output retrieved:
let output: EthereumSigningOutput = AnySigner.sign(input: input, coin: .ethereum)
print(" data: ", output.encoded.hexString)
For more details on Ethereum transactions, check the Ethereum documentation. A few resources are here:
- https://medium.com/@codetractio/inside-an-ethereum-transaction-fa94ffca912f
- https://kauri.io/article/7e79b6932f8a41a4bcbbd194fd2fcc3a/v2/ethereum-101-part-4-accounts-transactions-and-messages
- https://github.com/ethereumbook/ethereumbook/blob/develop/06transactions.asciidoc
Binance Chain is built upon cosmos-sdk, instead of Message
, transaction in Binance Chain is called Order
, Binance.proto shows all the orders that Wallet Core currently supports.
To sign a order, you need to use BinanceSigningInput
:
Field | Sample value | Description |
---|---|---|
chainID | Binance-Chain-Nile | Network id, use Binance-Chain-Tigris for mainnet (see node-info api) |
accountNumber | 51 | On chain account number. (see account api) |
sequence | 437412 | Order sequence starting from 0, always plus 1 for new order from account api |
source | 0 | BEP10 source id |
sendOrder | <sendOrder> | SendOrder contains inputs and outputs , see below sample code for more details |
A Swift sample code send order is shown below:
let privateKey = PrivateKey(data: Data(hexString: "95949f757db1f57ca94a5dff23314accbe7abee89597bf6a3c7382c84d7eb832")!)!
let publicKey = privateKey.getPublicKeySecp256k1(compressed: true)
let token = BinanceSendOrder.Token.with {
$0.denom = "BNB" // BNB or BEP2 token symbol
$0.amount = 1 // Amount, 1 BNB
}
// A.k.a from / sender
let orderInput = BinanceSendOrder.Input.with {
$0.address = CosmosAddress(hrp: .binance, publicKey: publicKey)!.keyHash
$0.coins = [token]
}
// A.k.a to / recipient
let orderOutput = BinanceSendOrder.Output.with {
$0.address = CosmosAddress(string: "bnb1hlly02l6ahjsgxw9wlcswnlwdhg4xhx38yxpd5")!.keyHash
$0.coins = [token]
}
let input = BinanceSigningInput.with {
$0.chainID = "Binance-Chain-Nile" // Testnet Chain id
$0.accountNumber = 0 // On chain account number
$0.sequence = 0 // Sequence number
$0.source = 0 // BEP10 source id
$0.privateKey = privateKey.data
$0.memo = ""
$0.sendOrder = BinanceSendOrder.with {
$0.inputs = [orderInput]
$0.outputs = [orderOutput]
}
}
let output: BinanceSigningOutput = AnySigner.sign(input: input, coin: .binance)
// encoded order to broadcast
print(output.encoded)
For more details please check the Binance Chain documentation:
- https://docs.binance.org/encoding.html
- https://docs.binance.org/api-reference/dex-api/paths.html#http-api
Consult the complete sample applications for more details.