Browse Source

Initial

tags/v1.0.0
dump_stack() 9 months ago
commit
7f0bec183b
Signed by: Mikhail Klementev <blame@dumpstack.io> GPG Key ID: BE44DA8C062D87DC
6 changed files with 472 additions and 0 deletions
  1. 21
    0
      LICENSE
  2. 39
    0
      README.md
  3. 229
    0
      bitcoin/bitcoin.go
  4. 131
    0
      bitcoin/bitcoin_test.go
  5. 47
    0
      cryptocurrency.go
  6. 5
    0
      shell.nix

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Mikhail Klementev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 39
- 0
README.md View File

@@ -0,0 +1,39 @@
# Cryptocurrency API

Stateless cryptocurrency API.

Usage:

package main

import (
"log"

"code.dumpstack.io/lib/cryptocurrency"
// "code.dumpstack.io/lib/cryptocurrency/bitcoin"
)

func main() {
// bitcoin.Testnet = true

seed, address, err := cryptocurrency.Bitcoin.GenWallet()
if err != nil {
log.Fatal(err)
}
log.Println(seed, address)

balance, err := cryptocurrency.Bitcoin.Balance(seed)
log.Println(balance)
if err != nil {
log.Fatal(err)
}

dest := "bc1q23fyuq7kmngrgqgp6yq9hk8a5q460f39m8nv87"
amount := float64(0.1)
tx, err := cryptocurrency.Bitcoin.Send(seed, dest, amount)
if err != nil {
// here it'll exit because there's no money inside new wallet
log.Fatal(err)
}
log.Println(tx)
}

+ 229
- 0
bitcoin/bitcoin.go View File

@@ -0,0 +1,229 @@
package bitcoin

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)

var Testnet = false

func init() {
if exec.Command("which", "electrum").Run() != nil {
panic("`electrum` is not found in $PATH")
}
}

func electrum(wallet string, args ...string) (output []byte, err error) {
params := []string{}
if Testnet {
params = append(params, "--testnet")
}
params = append(params, "--wallet", wallet)
params = append(params, args...)

return exec.Command("electrum", params...).Output()
}

func startDaemon(wallet string) (err error) {
output, err := electrum(wallet, "daemon", "start")
if err != nil {
return
}

output, err = electrum(wallet, "daemon", "load_wallet")
if err != nil {
return
}

for {
output, _ = electrum(wallet, "is_synchronized")
if string(output) == "true\n" {
break
}
time.Sleep(time.Second)
}
return
}

func stopDaemon(wallet string) (err error) {
_, err = electrum(wallet, "daemon", "stop")
return
}

func GenWallet() (seed, address string, err error) {
dir, err := ioutil.TempDir("/tmp/", "cryptocurrency_")
if err != nil {
return
}
defer os.RemoveAll(dir)

wallet := filepath.Join(dir, "wallet")

output, err := electrum(wallet, "create")
if err != nil {
return
}

var result struct{ Seed string }
err = json.Unmarshal(output, &result)
if err != nil {
return
}
seed = strings.Trim(result.Seed, " \r\n")

err = startDaemon(wallet)
if err != nil {
return
}
defer stopDaemon(wallet)

output, err = electrum(wallet, "getunusedaddress")
if err != nil {
return
}

address = strings.Trim(string(output), " \r\n")
return
}

func parseBalance(output []byte) (confirmed, unconfirmed float64, err error) {
var result struct{ Confirmed, Unconfirmed string }
err = json.Unmarshal(output, &result)
if err != nil {
return
}

confirmed, err = strconv.ParseFloat(result.Confirmed, 64)
if err != nil {
return
}

if result.Unconfirmed != "" {
unconfirmed, err = strconv.ParseFloat(result.Unconfirmed, 64)
if err != nil {
return
}
}

return
}

func Balance(seed string) (amount float64, err error) {
amount, unconfirmed, err := RawBalance(seed)
if unconfirmed < 0 {
amount += unconfirmed // subtraction
}
return
}

func RawBalance(seed string) (confirmed, unconfirmed float64, err error) {
dir, err := ioutil.TempDir("/tmp/", "cryptocurrency_")
if err != nil {
return
}
defer os.RemoveAll(dir)

wallet := filepath.Join(dir, "wallet")

output, err := electrum(wallet, "restore", seed)
if err != nil {
return
}

err = startDaemon(wallet)
if err != nil {
return
}
defer stopDaemon(wallet)

output, err = electrum(wallet, "getbalance")
if err != nil {
return
}

confirmed, unconfirmed, err = parseBalance(output)
return
}

func Validate(btc string) (valid bool, err error) {
output, err := electrum("", "validateaddress", btc)
if err != nil {
return
}

switch string(output) {
case "true\n":
valid = true
break
case "false\n":
valid = false
break
default:
err = errors.New("electrum output is invalid")
}
return
}

func send(seed, destination string, amount string) (tx string, err error) {
dir, err := ioutil.TempDir("/tmp/", "cryptocurrency_")
if err != nil {
return
}
defer os.RemoveAll(dir)

wallet := filepath.Join(dir, "wallet")

_, err = electrum(wallet, "restore", seed)
if err != nil {
return
}

err = startDaemon(wallet)
if err != nil {
return
}
defer stopDaemon(wallet)

output, err := electrum(wallet, "payto", destination, amount)
if err != nil {
return
}

var result struct {
Complete bool
Final bool
Hex string
}
err = json.Unmarshal(output, &result)
if err != nil {
return
}

if !result.Complete {
err = errors.New("Transaction is not complete")
return
}

output, err = electrum(wallet, "broadcast", result.Hex)
if err != nil {
return
}
tx = string(output)
return
}

func Send(seed, destination string, amount float64) (tx string, err error) {
return send(seed, destination, fmt.Sprintf("%.8f", amount))
}

func SendAll(seed, destination string) (tx string, err error) {
return send(seed, destination, "!")
}

+ 131
- 0
bitcoin/bitcoin_test.go View File

@@ -0,0 +1,131 @@
package bitcoin

import (
"testing"
"time"
)

func init() {
Testnet = true
}

func TestGenWallet(t *testing.T) {
seed, address, err := GenWallet()
if err != nil {
t.Fatal(err)
}

balance, err := Balance(seed)
if err != nil {
return
}

if balance != 0 {
t.Fatal("BINGO (balance of new wallet != 0)", seed, balance)
}

valid, err := Validate(address)
if err != nil || !valid {
t.Fatal("Generated address is invalid", address, err)
}
}

func TestValidate(t *testing.T) {
var addr string
if Testnet {
addr = "mpob68igSVfmaRyvucXHjJEdpbWG5Gt8dR"
} else {
addr = "1An9UvkeF1b57u448To7wqZ34HLEkSqCQ1"
}
valid, err := Validate(addr)
if err != nil {
t.Fatal(err)
}

if !valid {
t.Fatal("should be valid")
}

valid, err = Validate("WRONGAn9UvkeF1b57u448To7wqZ34HLEkSqCQ1")
if err != nil {
t.Fatal(err)
}

if valid {
t.Fatal("should be invalid")
}
}

func TestBalance(t *testing.T) {
// wallet with zero balance
seed := "differ come sugar drift clump athlete " +
"sweet fiscal uncle dilemma cage garbage"
balance, err := Balance(seed)
if err != nil {
t.Fatal(err)
}
if balance != 0 {
t.Fatal("BINGO (balance of test wallet != 0)", seed, balance)
}

_, err = Balance("some garbage instead of seed")
if err == nil {
t.Fatal("Balance does not returns error on invalid seed")
}

// wallet with some test btc inside
seed = "flag release number shift amazing bacon " +
"trend maximum lawsuit start traffic feel"
balance, err = Balance(seed)
if err != nil {
t.Fatal(err)
}
if balance == 0 {
t.Fatal("Balance of test wallet should not be zero", seed, balance)
}
}

func TestSend(t *testing.T) {
// wallet with some test btc inside
// do not forget to put some btc to
// n4DSCXMeKjRRjBQHHvKURsLtmrcyfijTnN
// from time to time
seed := "act sentence begin build tornado note " +
"then jungle jar govern bird dinner"
balance, err := Balance(seed)
if err != nil {
t.Fatal(err)
}

if balance == 0 {
t.Fatal("Please, open https://coinfaucet.eu/en/btc-testnet/ " +
"and put address `n4DSCXMeKjRRjBQHHvKURsLtmrcyfijTnN`")
}

newseed, address, err := GenWallet()
if err != nil {
t.Fatal(err)
}

_, err = Send(seed, address, 0.0000001)
if err != nil {
t.Fatal(err)
}

received := false
for start := time.Now(); time.Since(start) < time.Minute; {
_, unconfirmed, err := RawBalance(newseed)
if err != nil {
t.Fatal(err)
}

if unconfirmed != 0 {
received = true
break
}
}

if !received {
t.Fatal("Does not received btc")
}
}

+ 47
- 0
cryptocurrency.go View File

@@ -0,0 +1,47 @@
package cryptocurrency

import (
"errors"

"code.dumpstack.io/lib/cryptocurrency/bitcoin"
)

type Cryptocurrency int

const (
Bitcoin Cryptocurrency = iota
Ethereum
Monero
Cardano
)

func (t Cryptocurrency) GenWallet() (seed, address string, err error) {
switch t {
case Bitcoin:
seed, address, err = bitcoin.GenWallet()
return
}

err = errors.New("Not supported yet")
return
}

func (t Cryptocurrency) Balance(seed string) (amount float64, err error) {
switch t {
case Bitcoin:
return bitcoin.Balance(seed)
}

err = errors.New("Not supported yet")
return
}

func (t Cryptocurrency) Send(seed, dest string, amount float64) (tx string, err error) {
switch t {
case Bitcoin:
return bitcoin.Send(seed, dest, amount)
}

err = errors.New("Not supported yet")
return
}

+ 5
- 0
shell.nix View File

@@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:

with pkgs; mkShell {
buildInputs = [ which go electrum ];
}

Loading…
Cancel
Save