Envío de Bitcoin con Ruby – BloomX

Tiene 5BTC en su billetera, se divide en 4 direcciones diferentes. Sí, quiero decir, en realidad estoy diciendo direcciones, pero en realidad son "salidas de transacciones no gastadas". Quédese conmigo por ahora y lo señalaré más adelante.

1addr tiene 2 BTC2addr tiene 1.5BTC3addr tiene 0.5BTC4addr tiene 1BTC

Desea enviar 0.01BTC a alguien llamado Persona A, deberías usar algunos de tus BTC en esas 4 direcciones. Digamos que queremos usar 3addry Persona A te dio su dirección: 1some1. Su transacción se vería así:

Entradas 3addr = 0.5BTCOutputs 1some1 = 0.01BTC

Sin embargo, esto no está completo, entonces, ¿qué pasa con el 0.49BTC? Bueno, ¡deberías devolvértelo a ti mismo, por supuesto! De lo contrario, todo irá a los mineros. Lo mejor es generar una nueva dirección y ponerla allí. Llamemos a esa nueva dirección como 5addr. Ahora nuestra transacción se ve así:

Entradas 3addr = 0.5BTCOutputs 1some1 = 0.01BTC | 5addr = 0.49BTC

¿Qué pasa con esas tarifas de transacción? ¿Cómo los configuro?

La tarifa de transacción es la diferencia entre la suma de sus entradas y la suma de sus cantidades de salida. Digamos que las tarifas de transacción serían 0.000178BTC

Entonces la transacción final se vería así:

Entradas 3addr = 0.5BTCOutputs 1some1 = 0.01BTC El | 5addr = (0.49BTC – 0.000178BTC)

Si aún no lo entendió, ¡está bien! Si comprende mejor los conceptos cuando escribe el código y las pruebas, probablemente haga clic a medida que avanza ?

Primero, queremos establecer qué tipo de tarifas son "rápidas", para que podamos usar el número correcto al crear la transacción. Podemos rellenar este código dentro de una clase de servicio simple.

módulo Bitcoinzzz
clase GetFees

AVE_TX_BYTES = 250.0

def self.call
cliente = Bitcoiner.new () # ¿Cuál es la tarifa por 3 bloques?
respuesta = client.request ("Estimaciones SmartFfee", 3) si la respuesta ("errores").
# … hacer algo
final

# Estimacionesmartfee ​​es una llamada JPC RPC bitcoind
# devuelve la tarifa óptima por kB (1000 bytes), dada la
# número de bloques que desea que confirme su transacción
# #
# solo queremos la tarifa por 250bytes (tamaño promedio de transmisión).
# también puede calcular el tamaño de TX si lo desea.
tarifa = respuesta ("tarifa"). to_d * (AVE_TX_SIZE / 1000.0)
cuota
final

final
final

¿No podemos usar sendtoaddress? (sendtoaddress es una llamada JPC RPC bitcoind)

¡Sí tu puedes! Definitivamente, esa es la forma más fácil de hacer esto, pero si desea más control sobre las tarifas, se gastan los resultados; dividir lo que hace sendtoaddress sería un paso en esa dirección (también conocido como pequeños refactores que utilizan el método Ship of Theseus), ¡y también podemos aprender en el camino!

Así que aquí están los pasos para imitar sendtoaddress de manera similar

Obteniendo su dinero no gastado (¿Qué puedo usar para mis entradas?)

Para construir la transacción, necesitará obtener "resultados gastables". Para simplificar la explicación, aquí es de donde vendrá el BTC, una lista de direcciones no gastadas en la billetera bitcoind.

# listunspent es otra llamada RPC de bitcoind JSON, enumera todos los
# txs / direcciones anteriores que contiene su
unspent = client.request ("listunspent") unspent.inspect
# => (
{
"dirección": "1addr",
"cantidad": 2.0,
"confirmaciones": 1000,
"desc": "algún_texto",
"redeemScript": "some_redeem_script",
"seguro": cierto,
"scriptPubKey": "some_script_pubkey",
"solucionable": cierto,
"gastable": cierto,
"txid": "the_remote_txid",
"vout": 0
},
{
"dirección": "2addr",
"cantidad": 1.5,
"confirmaciones": 1000,
"desc": "algún_texto",
"redeemScript": "some_redeem_script",
"seguro": cierto,
"scriptPubKey": "some_script_pubkey",
"solucionable": cierto,
"gastable": cierto,
"txid": "the_remote_txid",
"vout": 0
},

)

¿Recuerdas que dije esto antes?

Sí, quiero decir, en realidad estoy diciendo direcciones, pero en realidad son "salidas de transacciones no gastadas". Quédese conmigo por ahora y lo señalaré más adelante.

A veces verás aquí 2 artículos con la misma dirección pero diferente vout valores. Significa que la dirección se usó varias veces. Entonces, lo que esta lista realmente representa son salidas de transacciones no gastadas (UTXO). Estas son transacciones anteriores en las que las salidas son direcciones que su bitcoind 'hot wallet' controla, por ejemplo, alguien que le envía a su billetera activa algunos BTC, o "cambia los montos" de las transacciones anteriores que realizó (más sobre eso más adelante)

Filtre lo que usará (¿Qué puedo usar para mis entradas?)

Presentado con esta lista, simplemente tome lo que se puede "gastar". Significa que esta transacción puede ser utilizada por bitcoind para enviar (debajo, todo lo que significa es que bitcoind tiene las claves para usar esa dirección).

# lo que desea que su destinatario reciba realmente
receivable_amount = 0.01 # tarifa de muestra de Bitcoinzzz :: GetFees. ()
tarifa = 0.000178 # filtro todo gastable
gastable = unspent.map do | output |
salida si salida ("gastable")
end.compact

Si envía 0.01BTC + 0.000178BTC, no necesitará todas las "salidas gastables", así que obtenga la cantidad correcta de salidas.

total_usable = 0 # cantidad total que gastará: 0.01 + 0.000178
shipping_amount = receivable_amount + fee # solo obtenga la cantidad correcta de salidas
usable = spendable.map do | output |
si total_usable <enviando_cantidad
total_usable + = salida ("cantidad")
salida
final
end.compact # basado en el ejemplo no utilizado, la matriz 'usable' solo contendrá el 1addr hash por ahora. Solo estas enviando 0.01BTC + 0.000178BTC de todas formas. 1addr contiene 2BTC

Obtener un cambio de dirección (¿Dónde lo enviaré? ¿Mis salidas?)

¿Recuerdas lo que dije antes sobre los resultados?

Deberás devolverte la diferencia. Más sobre eso más tarde.

Por supuesto, solo desea enviar 0.01BTC + 0.000178BTC (tarifas). En este momento, ¡tiene un valor de 0.5 BTC de (ins) que financia su transacción! Deberá enviar el resto a su billetera. Esto se llama una dirección de cambio, puede obtener una usando JSON RPC de bitcoind nuevamente.

getting_address = "SOME_ADDRESS_HERE" # getrawchangeaddress es otra llamada RPC de bitcoind JSON que proporciona
# usted una nueva dirección donde puede enviar el cambio a change_address = client.request (
"getrawchangeaddress",
"bech32", la adopción # bech32 sería muy agradable, menor tamaño de tx.
) # Vamos a calcular lo que nos enviaremos de regreso y lo que le enviaremos
# a la dirección_de_cambios
# #
# total_usable = 2BTC de 1addr
# shipping_amount = 0.01BTC + 0.000178 (honorarios) change_amount = total_usable – send_amount

Construir el hash de la transacción (poner mis salidas y entradas juntas)

Una vez que haya determinado de dónde obtendrá el dinero (entradas) y hacia dónde irá (salidas). Vamos a preparar un objeto json para crear una transacción

ins = usable.map do | output |
{
"txid" => salida ("txid"),
"vout" => salida ("vout"),
}
endouts = (
{destination_address => receivable_amount},
{change_address => change_amount},
)
inspeccionar
# => (
{"1some1" => 0.01},
{"change_address" => 1.98922},
) tx_to_submit = {ins: ins, outs: outs} # createrawtransaction construye su transacción lista para firmar.
# el resultado será una cadena codificada en hexadecimal
raw_tx = client.request (
"createrawtransaction",
tx_to_submit (: ins),
tx_to_submit (: salidas),
0, # tiempo de bloqueo
verdadero, # reemplazable
)

Firma la transacción

Después de obtener la transacción codificada en hexadecimal mediante createrawtransaction, fírmela.

# signrawtransactionwithwallet es una llamada JSON RPC bitcoind que
# firma tu transacción con las claves para esas direcciones
resp = client.request (
"signrawtransactionwithwallet",
raw_tx,
) if resp ("errores") presente?
raise StandardError, "Error al firmar la transacción – # {resp}"
endsigned_tx = resp ("hex")

Envía la transacción

¡Ahora puede enviar la transacción firmada al nodo bitcoind local y a la red!

# puede verificar el tx_id devuelto en un explorer como
# blockstream.info o .com
tx_id = client.request ("sendrawtransaction", firmado_tx)

Yey! Tomemos nuestro práctico vcr: {record:: once} Aquí hay una prueba simple para todo el trabajo que acabamos de hacer.

require "rails_helper" # Ponga todas las cosas que hicimos en una clase desacoplada para que sea fácil de probar. Aquí se supone que podemos escribir una prueba de integración para un Bitcoinzzz :: Enviar clase que hace todo lo que hablamos sobre el módulo Bitcoinzzz
RSpec.describe Enviar do let (: tarifa) {Bitcoinzzz :: GetFees. ()}
dejar (: cliente) hacer
Bitcoiner.new ()
final
let (: address) {client.request ("getnewaddress")} it (
"envía BTC",
vcr: {
registro: una vez,
match_requests_on:% i (método uri del cuerpo),
},
) hacer
current_balance = client.request ("getbalance") result_tx_id = describe_class. (
tx_fee_in_btc: tarifa,
dirección_de_destino: dirección,
cantidad_aceptable: 0.002,
) after_send_balance = client.request ("getbalance") # espera que las diferencias de saldo sean para tx_fee ya que
# lo devolvió a nosotros mismos
esperado_diff = balance_corriente – after_send_balance
esperar (esperado_diff.round (7) .to_d) .to eq honorario esperar (result_tx_id) .not_to be_nil
esperar (result_tx_id) .to be_a Cadena
remote_tx = client.request (
"getrawtransaction",
result_tx_id,
cierto, # detallado, te da json en lugar de una cadena hexadecimal
)

# analiza ese remote_tx y cumple tus expectativas
final

final
final

Ahí tienes! Ahora puede enviar Bitcoin con Ruby. Aunque, ¿qué pasa si esta clase de servicio se llama mucho? ¿Y al mismo tiempo?

Probablemente pueda usar algo como sidekiq-unique-jobs y alinear todas las solicitudes de retiro en una cola de trabajo para que no tenga condiciones de carrera. También puede tener validaciones de saldo al inicio a través de getbalance si la billetera todavía tiene dinero gastable y puede fallar el trabajo en consecuencia.

Hay más formas, pero definitivamente puede hacer de esta una clase de servicio segura para usar a través de la programación defensiva.

Otra implementación de siguiente nivel para esto es no confiar en la billetera de bitcoind para obtener los resultados no gastados, construir y firmar los hashes de transacciones. Esto le permite alimentar múltiples entradas provenientes de una billetera fría y teclas de múltiples sig de diferentes firmantes.

Puede usar bibliotecas de nivel inferior como bitcoin-ruby para lograr esto, pero tendrá que realizar un seguimiento de sus direcciones, claves y transacciones por su cuenta, también implementando / complementando las capacidades de billetera de bitcoind.

Esperemos que esta guía te haya facilitado cómo trabajar con Bitcoin y bitcoind.