Browse Source

initial commit

tags/v1.0.0
codesoap 8 months ago
commit
70638ceea6
15 changed files with 969 additions and 0 deletions
  1. 19
    0
      LICENSE
  2. 61
    0
      README.md
  3. 38
    0
      account.go
  4. 74
    0
      address.go
  5. 133
    0
      balance.go
  6. 196
    0
      block.go
  7. 64
    0
      change.go
  8. 10
    0
      config.go
  9. 8
    0
      go.mod
  10. 10
    0
      go.sum
  11. 81
    0
      main.go
  12. 15
    0
      new.go
  13. 40
    0
      process.go
  14. 103
    0
      send.go
  15. 117
    0
      util.go

+ 19
- 0
LICENSE View File

@@ -0,0 +1,19 @@
Copyright (c) 2021 Richard Ulmer

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 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.

+ 61
- 0
README.md View File

@@ -0,0 +1,61 @@
**WORK IN PROGRESS**

atto is a tiny Nano wallet, targeted at friends of the UNIX philosophy.
The design goal is to provide a minimal set of actions to manipulate
your Nano account. atto provides no means of inspecting transaction
history as it is completely stateless. Use a Nano crawler like
https://nanocrawler.cc/ if you need such info.

# Usage
```console
$ # The new command generates a new seed.
$ atto new
D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C
$ # Store it in your password manager:
$ pass insert nano
Enter password for nano: D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C
Retype password for nano: D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C

$ # The address command shows the address for a seed and account index.
$ pass nano | atto address
nano_3cyb3rwp5ba47t5jdzm5o7apeduppsgzw8ockn1dqt4xcqgapta6gh5htnnh

$ # The balance command will receive pending funds automatically.
$ pass nano | atto balance
Creating receive block for 1.025 from nano_34ymtnmhwseiex4eqf7nnf5wcyg44kknuuen5wwurm18ma91msf6e1pqo8hx... done
Creating receive block for 0.100 from nano_39nd8eksw1ia6aokn96z4uthocke47hfsx9gr31othm1nrfwnzmmaeehiccq... done
1.337 NANO

$ # Choosing a representative is important for keeping the network
$ # decentralized.
$ pass nano | atto representative nano_1jr699mk1fi6mxy1y76fmuyf3dgms8s5pzcsge5cyt1az93x4n18uxjenx93

$ # Careful with the send subcommand: No confirmation is required!
$ pass nano | atto send 0.1 nano_11zdqnjpisos53uighoaw95satm4ptdruck7xujbjcs44pbkkbw1h3zomns5
Creating send block (may take many minutes)... done

$ atto -h
Usage:
atto n[ew]
atto [-a ACCOUNT_INDEX] a[ddress]
atto [-a ACCOUNT_INDEX] b[alance]
atto [-a ACCOUNT_INDEX] r[epresentative] REPRESENTATIVE
atto [-a ACCOUNT_INDEX] s[end] AMOUNT RECEIVER

The new subcommand generates a new seed, which can later be used with
the other subcommands.

The address, balance, representative and send subcommands will expect
a seed as as the first line of their standard input. Showing the first
address of a newly generated key could work like this:
atto new | tee seed.txt | atto address

The address subcommand displays addresses for a seed, the balance
subcommand receives pending sends and shows the balance of an account,
the representative subcommand changes the account's representative and
the send subcommand sends funds to an address.

ACCOUNT_INDEX is an optional parameter, which must be a number between 0
and 4,294,967,295. It allows you to use multiple accounts derived from
the same seed. By default the account with index 0 is chosen.
```

+ 38
- 0
account.go View File

@@ -0,0 +1,38 @@
package main

import (
"encoding/json"
"fmt"
)

type accountInfo struct {
Error string `json:"error"`
Frontier string `json:"frontier"`
Representative string `json:"representative"`
Balance string `json:"balance"`
}

func getAccountInfo(address string) (info accountInfo, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "account_info",`+
`"account": "%s",`+
`"representative": "true"`+
`}`, address)
responseBytes, err := doRPC(requestBody)
if err != nil {
return
}
if err = json.Unmarshal(responseBytes, &info); err != nil {
return
}
// Need to check info.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if info.Error == "Account not found" {
info.Frontier = "0000000000000000000000000000000000000000000000000000000000000000"
info.Representative = defaultRepresentative
info.Balance = "0"
} else if info.Error != "" {
err = fmt.Errorf("could not fetch balance: %s", info.Error)
}
return
}

+ 74
- 0
address.go View File

@@ -0,0 +1,74 @@
package main

import (
"fmt"
"math/big"
"strings"

"filippo.io/edwards25519"
"golang.org/x/crypto/blake2b"
)

func printAddress() error {
seed, err := getSeed()
if err != nil {
return err
}
privateKey := getPrivateKey(seed, uint32(accountIndexFlag))
address, err := getAddress(privateKey)
if err != nil {
return err
}
fmt.Println(address)
return nil
}

func getAddress(privateKey *big.Int) (string, error) {
publicKey := derivePublicKey(privateKey)
base32PublicKey := base32Encode(publicKey)

publicKeyBytes := make([]byte, 32, 32)
publicKey.FillBytes(publicKeyBytes)
hasher, err := blake2b.New(5, nil)
if err != nil {
return "", err
}
if _, err := hasher.Write(publicKeyBytes); err != nil {
return "", err
}
hashBytes := hasher.Sum(nil)
base32Hash := base32Encode(big.NewInt(0).SetBytes(revertBytes(hashBytes)))

address := "nano_" +
strings.Repeat("1", 52-len(base32PublicKey)) + base32PublicKey +
strings.Repeat("1", 8-len(base32Hash)) + base32Hash
return address, nil
}

func getPrivateKey(seed *big.Int, index uint32) *big.Int {
seedBytes := make([]byte, 32, 32)
seed.FillBytes(seedBytes)
indexBytes := make([]byte, 4, 4)
big.NewInt(int64(index)).FillBytes(indexBytes)
in := append(seedBytes, indexBytes...)
privateKeyBytes := blake2b.Sum256(in)
return big.NewInt(0).SetBytes(privateKeyBytes[:])
}

func derivePublicKey(privateKey *big.Int) *big.Int {
privateKeyBytes := make([]byte, 32, 32)
privateKey.FillBytes(privateKeyBytes)
hashBytes := blake2b.Sum512(privateKeyBytes)
scalar := edwards25519.NewScalar().SetBytesWithClamping(hashBytes[:32])
publicKeyBytes := edwards25519.NewIdentityPoint().ScalarBaseMult(scalar).Bytes()
return big.NewInt(0).SetBytes(publicKeyBytes)
}

func getPublicKeyFromAddress(address string) (*big.Int, error) {
if len(address) == 64 {
return base32Decode(address[4:56])
} else if len(address) == 65 {
return base32Decode(address[5:57])
}
return nil, fmt.Errorf("could not parse address %s", address)
}

+ 133
- 0
balance.go View File

@@ -0,0 +1,133 @@
package main

import (
"encoding/json"
"fmt"
"math/big"
"os"
)

type pending struct {
Error string `json:"error"`
Blocks pendingBlocks `json:"blocks"`
}

type pendingBlocks map[string]pendingBlockSource

// UnmarshalJSON just unmarshals a list of strings, but
// interprets an empty string as an empty list. This is
// necessary due to a bug in the Nano node implementation. See
// https://github.com/nanocurrency/nano-node/issues/3161.
func (b *pendingBlocks) UnmarshalJSON(in []byte) error {
if string(in) == `""` {
return nil
}
var raw map[string]pendingBlockSource
err := json.Unmarshal(in, &raw)
*b = pendingBlocks(raw)
return err
}

type pendingBlockSource struct {
Amount string `json:"amount"`
Source string `json:"source"`
}

func printBalance() error {
seed, err := getSeed()
if err != nil {
return err
}
privateKey := getPrivateKey(seed, uint32(accountIndexFlag))
address, err := getAddress(privateKey)
if err != nil {
return err
}

info, err := getAccountInfo(address)
if err != nil {
return err
}
updatedBalance, err := receivePendingSends(info, privateKey)
if err != nil {
return err
}
fmt.Println(rawToNanoString(updatedBalance))
return nil
}

func receivePendingSends(info accountInfo, privateKey *big.Int) (updatedBalance *big.Int, err error) {
updatedBalance, ok := big.NewInt(0).SetString(info.Balance, 10)
if !ok {
err = fmt.Errorf("cannot parse '%s' as an integer", info.Balance)
return
}
address, err := getAddress(privateKey)
if err != nil {
return
}
sends, err := getPendingSends(address)
if err != nil {
return
}
previousBlock := info.Frontier
for blockHash, source := range sends {
amount, ok := big.NewInt(0).SetString(source.Amount, 10)
if !ok {
err = fmt.Errorf("cannot parse '%s' as an integer", source.Amount)
return
}
updatedBalance = updatedBalance.Add(updatedBalance, amount)
txt := "Creating receive block for %s from %s... "
fmt.Fprintf(os.Stderr, txt, rawToNanoString(amount), source.Source)

block := block{
Type: "state",
Account: address,
Previous: previousBlock,
Representative: info.Representative,
Balance: updatedBalance.String(),
Link: blockHash,
}
if err = block.sign(privateKey); err != nil {
return
}
if err = block.addWork(receiveWorkThreshold, privateKey); err != nil {
return
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "receive",
Block: block,
}
if err = doProcessRPCCall(process); err != nil {
return
}

fmt.Fprintln(os.Stderr, "done")
previousBlock = block.Hash
}
return
}

func getPendingSends(address string) (sends pendingBlocks, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "pending", `+
`"account": "%s", `+
`"include_only_confirmed": "true", `+
`"source": "true"`+
`}`, address)
responseBytes, err := doRPC(requestBody)
if err != nil {
return
}
var pending pending
err = json.Unmarshal(responseBytes, &pending)
// Need to check pending.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if err == nil && pending.Error != "" {
err = fmt.Errorf("could not fetch unreceived sends: %s", pending.Error)
}
return pending.Blocks, err
}

+ 196
- 0
block.go View File

@@ -0,0 +1,196 @@
package main

import (
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"

"filippo.io/edwards25519"
"golang.org/x/crypto/blake2b"
)

type block struct {
Type string `json:"type"`
Account string `json:"account"`
Previous string `json:"previous"`
Representative string `json:"representative"`
Balance string `json:"balance"`
Link string `json:"link"`
Signature string `json:"signature"`
Work string `json:"work"`
Hash string `json:"-"`
}

type workerResult struct {
hashNumber uint64
nonce uint64
}

func (b *block) sign(privateKey *big.Int) error {
// Look at https://nanoo.tools/block for a reference. This
// implementation based on the one from github.com/iotaledger/iota.go.

publicKey := derivePublicKey(privateKey)
hash, err := b.hash(publicKey)
if err != nil {
return err
}
b.Hash = fmt.Sprintf("%064X", hash)
signature := make([]byte, 64, 64)

privateKeyBytes := make([]byte, 32, 32)
privateKey.FillBytes(privateKeyBytes)
h, err := blake2b.New512(nil)
if err != nil {
return err
}
h.Write(privateKeyBytes)

var digest1, messageDigest, hramDigest [64]byte
h.Sum(digest1[:0])

s := new(edwards25519.Scalar).SetBytesWithClamping(digest1[:32])

h.Reset()
h.Write(digest1[32:])
h.Write(hash)
h.Sum(messageDigest[:0])

rReduced := new(edwards25519.Scalar).SetUniformBytes(messageDigest[:])
R := new(edwards25519.Point).ScalarBaseMult(rReduced)

encodedR := R.Bytes()

h.Reset()
h.Write(encodedR[:])
publicKeyBytes := make([]byte, 32, 32)
publicKey.FillBytes(publicKeyBytes)
h.Write(publicKeyBytes)
h.Write(hash)
h.Sum(hramDigest[:0])

kReduced := new(edwards25519.Scalar).SetUniformBytes(hramDigest[:])
S := new(edwards25519.Scalar).MultiplyAdd(kReduced, s, rReduced)

copy(signature[:], encodedR[:])
copy(signature[32:], S.Bytes())

b.Signature = fmt.Sprintf("%0128X", signature)
return nil
}

func (b *block) hash(publicKey *big.Int) ([]byte, error) {
msg := make([]byte, 176, 176)

msg[31] = 0x6 // block preamble

publicKeyBytes := make([]byte, 32, 32)
publicKey.FillBytes(publicKeyBytes)
copy(msg[32:64], publicKeyBytes)

previous, err := hex.DecodeString(b.Previous)
if err != nil {
return []byte{}, err
}
copy(msg[64:96], previous)

representative, err := getPublicKeyFromAddress(b.Representative)
representativeBytes := make([]byte, 32, 32)
representative.FillBytes(representativeBytes)
if err != nil {
return []byte{}, err
}
copy(msg[96:128], representativeBytes)

balance, ok := big.NewInt(0).SetString(b.Balance, 10)
if !ok {
return []byte{}, fmt.Errorf("cannot parse '%s' as an integer", b.Balance)
}
balanceBytes := make([]byte, 16, 16)
balance.FillBytes(balanceBytes)
copy(msg[128:144], balanceBytes)

link, err := hex.DecodeString(b.Link)
if err != nil {
return []byte{}, err
}
copy(msg[144:176], link)

hash := blake2b.Sum256(msg)
return hash[:], nil
}

func (b *block) addWork(workThreshold uint64, privateKey *big.Int) (err error) {
var suffix []byte
if b.Previous == "0000000000000000000000000000000000000000000000000000000000000000" {
publicKey := derivePublicKey(privateKey)
suffix = make([]byte, 32, 32)
publicKey.FillBytes(suffix)
} else {
suffix, err = hex.DecodeString(b.Previous)
if err != nil {
return
}
}
nonce, err := findNonce(workThreshold, suffix)
if err != nil {
return
}
b.Work = fmt.Sprintf("%016x", nonce)
return
}

func findNonce(workThreshold uint64, suffix []byte) (uint64, error) {
results := make(chan workerResult)
quit := make(chan bool, workerRoutines)
errs := make(chan error, workerRoutines)
for i := 0; i < workerRoutines; i++ {
go calculateHashes(suffix, uint64(i), results, quit, errs)
}
for {
select {
case result := <-results:
if result.hashNumber >= workThreshold {
for i := 0; i < workerRoutines; i++ {
quit <- true
}
return result.nonce, nil
}
case err := <-errs:
for i := 0; i < workerRoutines; i++ {
quit <- true
}
return 0, err
}
}
}

func calculateHashes(suffix []byte, nonce uint64, results chan workerResult, quit chan bool, errs chan error) {
nonceBytes := make([]byte, 8)
hasher, err := blake2b.New(8, nil)
if err != nil {
errs <- err
return
}
for {
select {
case <-quit:
return
default:
binary.LittleEndian.PutUint64(nonceBytes, nonce)
_, err := hasher.Write(append(nonceBytes, suffix...))
if err != nil {
errs <- err
return
}
hashBytes := hasher.Sum(nil)
results <- workerResult{
hashNumber: binary.LittleEndian.Uint64(hashBytes),
nonce: nonce,
}
hasher.Reset()
nonce += uint64(workerRoutines)
}
}
}

+ 64
- 0
change.go View File

@@ -0,0 +1,64 @@
package main

import (
"flag"
"fmt"
"math/big"
"os"
)

func changeRepresentative() error {
seed, err := getSeed()
if err != nil {
return err
}
privateKey := getPrivateKey(seed, uint32(accountIndexFlag))
address, err := getAddress(privateKey)
if err != nil {
return err
}
info, err := getAccountInfo(address)
if err != nil {
return err
}
if info.Frontier == "0000000000000000000000000000000000000000000000000000000000000000" {
return fmt.Errorf("account has not yet been opened")
}
representative := flag.Arg(1)
fmt.Fprintf(os.Stderr, "Creating change block (may take many minutes)... ")
err = changeRepresatativeOfAccount(info, representative, privateKey)
if err != nil {
fmt.Fprintln(os.Stderr, "")
return err
}
fmt.Fprintln(os.Stderr, "done")
return nil
}

func changeRepresatativeOfAccount(info accountInfo, representative string, privateKey *big.Int) error {
address, err := getAddress(privateKey)
if err != nil {
return err
}
block := block{
Type: "state",
Account: address,
Previous: info.Frontier,
Representative: representative,
Balance: info.Balance,
Link: "0000000000000000000000000000000000000000000000000000000000000000",
}
if err = block.sign(privateKey); err != nil {
return err
}
if err = block.addWork(changeWorkThreshold, privateKey); err != nil {
return err
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "change",
Block: block,
}
return doProcessRPCCall(process)
}

+ 10
- 0
config.go View File

@@ -0,0 +1,10 @@
package main

var (
nodeUrl = "https://mynano.ninja/api/node"
defaultRepresentative = "nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz"
sendWorkThreshold uint64 = 0xfffffff800000000
changeWorkThreshold uint64 = 0xfffffff800000000
receiveWorkThreshold uint64 = 0xfffffe0000000000
workerRoutines = 512
)

+ 8
- 0
go.mod View File

@@ -0,0 +1,8 @@
module github.com/codesoap/atto

go 1.15

require (
filippo.io/edwards25519 v1.0.0-beta.3
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
)

+ 10
- 0
go.sum View File

@@ -0,0 +1,10 @@
filippo.io/edwards25519 v1.0.0-beta.3 h1:WQxB0FH5NzrhciInJ30bgL3soLng3AbdI651yQuVlCs=
filippo.io/edwards25519 v1.0.0-beta.3/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

+ 81
- 0
main.go View File

@@ -0,0 +1,81 @@
package main

import (
"flag"
"fmt"
"os"
)

var usage = `Usage:
atto n[ew]
atto [-a ACCOUNT_INDEX] a[ddress]
atto [-a ACCOUNT_INDEX] b[alance]
atto [-a ACCOUNT_INDEX] r[epresentative] REPRESENTATIVE
atto [-a ACCOUNT_INDEX] s[end] AMOUNT RECEIVER

The new subcommand generates a new seed, which can later be used with
the other subcommands.

The address, balance, representative and send subcommands will expect
a seed as as the first line of their standard input. Showing the first
address of a newly generated key could work like this:
atto new | tee seed.txt | atto address

The address subcommand displays addresses for a seed, the balance
subcommand receives pending sends and shows the balance of an account,
the representative subcommand changes the account's representative and
the send subcommand sends funds to an address.

ACCOUNT_INDEX is an optional parameter, which must be a number between 0
and 4,294,967,295. It allows you to use multiple accounts derived from
the same seed. By default the account with index 0 is chosen.
`

var accountIndexFlag uint

func init() {
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
flag.UintVar(&accountIndexFlag, "a", 0, "")
flag.Parse()
if flag.NArg() < 1 {
flag.Usage()
os.Exit(1)
}
var ok bool
switch flag.Arg(0)[:1] {
case "n":
ok = flag.NArg() == 1
case "a":
ok = flag.NArg() == 1
case "b":
ok = flag.NArg() == 1
case "r":
ok = flag.NArg() == 2
case "s":
ok = flag.NArg() == 3
}
if !ok {
flag.Usage()
os.Exit(1)
}
}

func main() {
var err error
switch flag.Arg(0)[:1] {
case "n":
err = printNewSeed()
case "a":
err = printAddress()
case "b":
err = printBalance()
case "r":
err = changeRepresentative()
case "s":
err = sendFunds()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
}

+ 15
- 0
new.go View File

@@ -0,0 +1,15 @@
package main

import (
"crypto/rand"
"fmt"
)

func printNewSeed() error {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return err
}
fmt.Printf("%X\n", b)
return nil
}

+ 40
- 0
process.go View File

@@ -0,0 +1,40 @@
package main

import (
"encoding/json"
"fmt"
)

type process struct {
Action string `json:"action"`
JsonBlock string `json:"json_block"`
Subtype string `json:"subtype"`
Block block `json:"block"`
}

type processResponse struct {
Error string `json:"error"`
}

func doProcessRPCCall(process process) error {
var requestBody, responseBytes []byte
requestBody, err := json.Marshal(process)
if err != nil {
return err
}
responseBytes, err = doRPC(string(requestBody))
if err != nil {
return err
}
var processResponse processResponse
err = json.Unmarshal(responseBytes, &processResponse)
if err != nil {
return err
}
// Need to check pending.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if processResponse.Error != "" {
err = fmt.Errorf("could not publish block: %s", processResponse.Error)
}
return err
}

+ 103
- 0
send.go View File

@@ -0,0 +1,103 @@
package main

import (
"flag"
"fmt"
"math/big"
"os"
"strings"
)

func sendFunds() error {
seed, err := getSeed()
if err != nil {
return err
}
privateKey := getPrivateKey(seed, uint32(accountIndexFlag))
address, err := getAddress(privateKey)
if err != nil {
return err
}
info, err := getAccountInfo(address)
if err != nil {
return err
}
if info.Frontier == "0000000000000000000000000000000000000000000000000000000000000000" {
return fmt.Errorf("account has not yet been opened")
}
amount := flag.Arg(1)
recipient := flag.Arg(2)
fmt.Fprintf(os.Stderr, "Creating send block (may take many minutes)... ")
err = sendFundsToAccount(info, amount, recipient, privateKey)
if err != nil {
fmt.Fprintln(os.Stderr, "")
return err
}
fmt.Fprintln(os.Stderr, "done")
return nil
}

func sendFundsToAccount(info accountInfo, amount, recipient string, privateKey *big.Int) error {
address, err := getAddress(privateKey)
if err != nil {
return err
}
balance, err := getBalanceAfterSend(info.Balance, amount)
if err != nil {
return err
}
recipientBytes := make([]byte, 32, 32)
recipientNumber, err := getPublicKeyFromAddress(recipient)
if err != nil {
return err
}
recipientNumber.FillBytes(recipientBytes)
block := block{
Type: "state",
Account: address,
Previous: info.Frontier,
Representative: info.Representative,
Balance: balance.String(),
Link: fmt.Sprintf("%064X", recipientBytes),
}
if err = block.sign(privateKey); err != nil {
return err
}
if err = block.addWork(sendWorkThreshold, privateKey); err != nil {
return err
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "send",
Block: block,
}
return doProcessRPCCall(process)
}

// getBalanceAfterSend expects oldBalance to be raw and amount to be
// Nano. amount is converted to raw and subtracted from oldBalance, the
// result is returned.
func getBalanceAfterSend(oldBalance string, amount string) (*big.Int, error) {
balance, ok := big.NewInt(0).SetString(oldBalance, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an integer", oldBalance)
return nil, err
}
missingZerosUntilRaw := 30
i := strings.Index(amount, ".")
if i > -1 {
missingZerosUntilRaw -= len(amount) - i - 1
if missingZerosUntilRaw < 0 {
return nil, fmt.Errorf("'%s' has too many decimal places", amount)
}
}
amount = strings.Replace(amount, ".", "", 1)
amount += strings.Repeat("0", missingZerosUntilRaw)
amountNumber, ok := big.NewInt(0).SetString(amount, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an interger", amount)
return nil, err
}
return balance.Sub(balance, amountNumber), nil
}

+ 117
- 0
util.go View File

@@ -0,0 +1,117 @@
package main

import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"os"
"strings"
)

// getSeed takes the first line of the standard input and interprets it
// as a hexadecimal representation of a 32byte seed.
func getSeed() (*big.Int, error) {
in := bufio.NewReader(os.Stdin)
firstLine, err := in.ReadString('\n')
if err != nil {
return nil, err
}
seed, ok := big.NewInt(0).SetString(strings.TrimSpace(firstLine), 16)
if !ok {
return seed, errors.New("could not parse seed")
}
return seed, nil
}

func base32Encode(in *big.Int) string {
alphabet := []byte("13456789abcdefghijkmnopqrstuwxyz")
bigZero := big.NewInt(0)
bigRadix := big.NewInt(32)
num := big.NewInt(0).SetBytes(in.Bytes())
out := make([]byte, 0)
mod := new(big.Int)
for num.Cmp(bigZero) > 0 {
num.DivMod(num, bigRadix, mod)
out = append(out, alphabet[mod.Int64()])
}
for i := 0; i < len(out)/2; i++ {
out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i]
}
return string(out)
}

func base32Decode(in string) (*big.Int, error) {
reverseAlphabet := map[rune]*big.Int{}
reverseAlphabet['1'] = big.NewInt(0)
reverseAlphabet['3'] = big.NewInt(1)
reverseAlphabet['4'] = big.NewInt(2)
reverseAlphabet['5'] = big.NewInt(3)
reverseAlphabet['6'] = big.NewInt(4)
reverseAlphabet['7'] = big.NewInt(5)
reverseAlphabet['8'] = big.NewInt(6)
reverseAlphabet['9'] = big.NewInt(7)
reverseAlphabet['a'] = big.NewInt(8)
reverseAlphabet['b'] = big.NewInt(9)
reverseAlphabet['c'] = big.NewInt(10)
reverseAlphabet['d'] = big.NewInt(11)
reverseAlphabet['e'] = big.NewInt(12)
reverseAlphabet['f'] = big.NewInt(13)
reverseAlphabet['g'] = big.NewInt(14)
reverseAlphabet['h'] = big.NewInt(15)
reverseAlphabet['i'] = big.NewInt(16)
reverseAlphabet['j'] = big.NewInt(17)
reverseAlphabet['k'] = big.NewInt(18)
reverseAlphabet['m'] = big.NewInt(19)
reverseAlphabet['n'] = big.NewInt(20)
reverseAlphabet['o'] = big.NewInt(21)
reverseAlphabet['p'] = big.NewInt(22)
reverseAlphabet['q'] = big.NewInt(23)
reverseAlphabet['r'] = big.NewInt(24)
reverseAlphabet['s'] = big.NewInt(25)
reverseAlphabet['t'] = big.NewInt(26)
reverseAlphabet['u'] = big.NewInt(27)
reverseAlphabet['w'] = big.NewInt(28)
reverseAlphabet['x'] = big.NewInt(29)
reverseAlphabet['y'] = big.NewInt(30)
reverseAlphabet['z'] = big.NewInt(31)
out := big.NewInt(0)
radix := big.NewInt(32)
for _, r := range in {
out = out.Mul(out, radix)
val, ok := reverseAlphabet[r]
if !ok {
return out, fmt.Errorf("'%c' is no legal base32 character", r)
}
out = out.Add(out, val)
}
return out, nil
}

func revertBytes(in []byte) []byte {
for i := 0; i < len(in)/2; i++ {
in[i], in[len(in)-1-i] = in[len(in)-1-i], in[i]
}
return in
}

func doRPC(requestBody string) (responseBytes []byte, err error) {
resp, err := http.Post(nodeUrl, "application/json", strings.NewReader(requestBody))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
err = fmt.Errorf("received unexpected HTTP return code %d", resp.StatusCode)
return
}
return ioutil.ReadAll(resp.Body)
}

func rawToNanoString(raw *big.Int) string {
rawPerKnano, _ := big.NewInt(0).SetString("1000000000000000000000000000", 10)
balance := big.NewInt(0).Div(raw, rawPerKnano).Uint64()
return fmt.Sprintf("%d.%03d NANO", balance/1000, balance%1000)
}

Loading…
Cancel
Save