Skip to content

Commit

Permalink
feat: add basic wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
classicalliu committed Mar 22, 2019
1 parent 5b141a8 commit 2e14b49
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ Metrics/AbcSize:
Metrics/MethodLength:
CountComments: false
Max: 50

Metrics/LineLength:
Max: 120

Metrics/ClassLength:
Max: 120
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ PATH
remote: .
specs:
ckb-sdk-ruby (0.1.0)
bitcoin-secp256k1 (~> 0.5.0)
rbnacl (~> 6.0, >= 6.0.1)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.0)
bitcoin-secp256k1 (0.5.0)
ffi (>= 1.9.25)
coderay (1.1.2)
diff-lcs (1.3)
ffi (1.10.0)
jaro_winkler (1.5.2)
method_source (0.9.2)
parallel (1.14.0)
Expand All @@ -20,6 +25,8 @@ GEM
psych (3.1.0)
rainbow (3.0.0)
rake (10.5.0)
rbnacl (6.0.1)
ffi
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
Expand Down
3 changes: 3 additions & 0 deletions ckb-sdk-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rubocop", "~> 0.66.0"
spec.add_development_dependency "pry", "~> 0.12.2"

spec.add_dependency "rbnacl", "~> 6.0", ">= 6.0.1"
spec.add_dependency "bitcoin-secp256k1", "~> 0.5.0"
end
3 changes: 3 additions & 0 deletions lib/ckb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

require "ckb/version"
require "ckb/rpc"
require "ckb/blake2b"
require "ckb/utils"
require "ckb/wallet"

module CKB
class Error < StandardError; end
Expand Down
36 changes: 36 additions & 0 deletions lib/ckb/blake2b.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "rbnacl"

module CKB
class Blake2b
DEFAULT_OPTIONS = {
personal: "ckb-default-hash",
digest_size: 32
}.freeze

def initialize(_opts = {})
@blake2b = self.class.generate
end

# @param [String] string, not bin
def update(message)
@blake2b.update(message)
@blake2b
end

alias << update

def digest
@blake2b.digest
end

def self.generate(_opts = {})
::RbNaCl::Hash::Blake2b.new(DEFAULT_OPTIONS.dup)
end

def self.digest(message)
::RbNaCl::Hash::Blake2b.digest(message, DEFAULT_OPTIONS.dup)
end
end
end
16 changes: 16 additions & 0 deletions lib/ckb/rpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@
module CKB
class RPC
attr_reader :uri
attr_reader :system_script_out_point
attr_reader :system_script_cell_hash

DEFAULT_URL = "http://localhost:8114"

def initialize(host: DEFAULT_URL)
@uri = URI(host)
end

# @param out_point [Hash] { hash: "0x...", index: 0 }
# @param cell_hash [String] "0x..."
def set_system_script_cell(out_point, cell_hash)
@system_script_out_point = out_point
@system_script_cell_hash = cell_hash
end

def system_script_cell
{
out_point: system_script_out_point,
cell_hash: system_script_cell_hash
}
end

def genesis_block
@genesis_block ||= get_block(genesis_block_hash)
end
Expand Down
100 changes: 100 additions & 0 deletions lib/ckb/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require "secp256k1"

module CKB
module Utils
def self.hex_to_bin(hex)
hex = hex[2..-1] if hex.start_with?("0x")
[hex].pack("H*")
end

def self.bin_to_hex(bin)
bin.unpack1("H*")
end

def self.bin_to_prefix_hex(bin)
"0x#{bin_to_hex(bin)}"
end

def self.extract_pubkey_bin(privkey_bin)
Secp256k1::PrivateKey.new(privkey: privkey_bin).pubkey.serialize
end

def self.json_script_to_type_hash(script)
blake2b = CKB::Blake2b.new
blake2b << hex_to_bin(script[:reference]) if script[:reference]
blake2b << "|"
blake2b << script[:binary] if script[:binary]
signed_args = script[:signed_args] || []
signed_args.each do |arg|
blake2b << arg
end
bin_to_prefix_hex(blake2b.digest)
end

def self.sign_sighash_all_inputs(inputs, outputs, privkey)
blake2b = CKB::Blake2b.new
sighash_type = 0x1.to_s
blake2b.update(sighash_type)
inputs.each do |input|
previous_output = input[:previous_output]
blake2b.update(hex_to_bin(previous_output[:hash]))
blake2b.update(previous_output[:index].to_s)
blake2b.update(
hex_to_bin(
json_script_to_type_hash(input[:unlock])
)
)
end
outputs.each do |output|
blake2b.update(output[:capacity].to_s)
blake2b.update(hex_to_bin(output[:lock]))
next unless output[:type]

blake2b.update(
hex_to_bin(
json_script_to_type_hash(output[:type])
)
)
end
key = Secp256k1::PrivateKey.new(privkey: privkey)
signature_bin = key.ecdsa_serialize(
key.ecdsa_sign(blake2b.digest, raw: true)
)
signature_hex = bin_to_hex(signature_bin)

inputs.map do |input|
unlock = input[:unlock].merge(args: [signature_hex, sighash_type])
input.merge(unlock: unlock)
end
end

# In Ruby, bytes are represented using String,
# since JSON has no native byte arrays,
# CKB convention bytes passed with a "0x" prefix hex encoding,
# hence we have to do type conversions here.
def self.normalize_tx_for_json!(transaction)
transaction[:inputs].each do |input|
unlock = input[:unlock]
unlock[:args] = unlock[:args].map { |arg| bin_to_prefix_hex(arg) }
unlock[:signed_args] = unlock[:signed_args].map { |arg| bin_to_prefix_hex(arg) }
next unless unlock[:binary]

unlock[:binary] = bin_to_prefix_hex(unlock[:binary])
end

transaction[:outputs].each do |output|
output[:data] = bin_to_prefix_hex(output[:data])
next unless output[:type]

type = output[:type]
type[:args] = type[:args].map { |arg| bin_to_prefix_hex(arg) }
type[:signed_args] = type[:signed_args].map { |arg| bin_to_prefix_hex(arg) }
type[:binary] = bin_to_prefix_hex(type[:binary]) if type[:binary]
end

transaction
end
end
end
145 changes: 145 additions & 0 deletions lib/ckb/wallet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# frozen_string_literal: true

# rubocop:disable Naming/AccessorMethodName

require "secp256k1"

module CKB
MIN_CELL_CAPACITY = 40

class Wallet
attr_reader :rpc
# privkey is a bin string
attr_reader :privkey

# @param rpc [CKB::RPC]
# @param privkey [String] bin string
def initialize(rpc, privkey)
raise ArgumentError, "invalid privkey!" unless privkey.instance_of?(String) && privkey.size == 32

@rpc = rpc
@privkey = privkey
end

# @param rpc [CKB::RPC]
# @param privkey_hex [String] hex string
#
# @return [CKB::Wallet]
def self.from_hex(rpc, privkey_hex)
new(rpc, CKB::Utils.hex_to_bin(privkey_hex))
end

def address
verify_type_hash
end

def get_unspent_cells
to = rpc.get_tip_block_number
results = []
current_from = 1
while current_from <= to
current_to = [current_from + 100, to].min
cells = rpc.get_cells_by_type_hash(address, current_from, current_to)
results.concat(cells)
current_from = current_to + 1
end
results
end

def get_balance
get_unspent_cells.map { |cell| cell[:capacity] }.reduce(0, &:+)
end

def generate_tx(target_address, capacity)
i = gather_inputs(capacity, MIN_CELL_CAPACITY)
input_capacities = i.capacities

outputs = [
{
capacity: capacity,
data: "",
lock: target_address
}
]
if input_capacities > capacity
outputs << {
capacity: input_capacities - capacity,
data: "",
lock: address
}
end
{
version: 0,
deps: [rpc.system_script_out_point],
inputs: CKB::Utils.sign_sighash_all_inputs(i.inputs, outputs, privkey),
outputs: outputs
}
end

# @param target_address [String] "0x..."
# @param capacity [Integer]
def send_capacity(target_address, capacity)
tx = generate_tx(target_address, capacity)
send_transaction_bin(tx)
end

# @param hash_hex [String] "0x..."
def get_transaction(hash_hex)
rpc.get_transaction(hash_hex)
end

private

def send_transaction_bin(transaction)
transaction = CKB::Utils.normalize_tx_for_json!(transaction)
rpc.send_transaction(transaction)
end

def gather_inputs(capacity, min_capacity)
raise "capacity cannot be less than #{min_capacity}" if capacity < min_capacity

input_capacities = 0
inputs = []
get_unspent_cells.each do |cell|
input = {
previous_output: cell[:out_point],
unlock: verify_script_json_object
}
inputs << input
input_capacities += cell[:capacity]

break if input_capacities >= capacity && (input_capacities - capacity) >= min_capacity
end

raise "Not enough capacity!" if input_capacities < capacity

OpenStruct.new(inputs: inputs, capacities: input_capacities)
end

def pubkey
CKB::Utils.bin_to_hex(pubkey_bin)
end

def pubkey_bin
CKB::Utils.extract_pubkey_bin(privkey)
end

def verify_script_json_object
{
version: 0,
reference: rpc.system_script_cell_hash,
signed_args: [
# We could of course just hash raw bytes, but since right now CKB
# CLI already uses this scheme, we stick to the same way for compatibility
pubkey
]
}
end

def verify_type_hash
@verify_type_hash ||= CKB::Utils.json_script_to_type_hash(verify_script_json_object)
end
end
end

# rubocop:enable Naming/AccessorMethodName
Loading

0 comments on commit 2e14b49

Please sign in to comment.