Browse Source

Make atto a library

tags/v1.3.0-beta
codesoap 2 months ago
parent
commit
ba37fbffc9
15 changed files with 337 additions and 578 deletions
  1. 8
    5
      README.md
  2. 60
    49
      account.go
  3. 133
    0
      account_info.go
  4. 0
    23
      address.go
  5. 0
    128
      balance.go
  6. 66
    51
      block.go
  7. 0
    51
      change.go
  8. 0
    17
      config.go
  9. 8
    6
      ed25519.go
  10. 0
    91
      main.go
  11. 0
    15
      new.go
  12. 47
    0
      pending.go
  13. 5
    5
      process.go
  14. 0
    130
      send.go
  15. 10
    7
      util.go

+ 8
- 5
README.md View File

@@ -1,5 +1,8 @@
[![GoDoc](https://godoc.org/github.com/codesoap/atto?status.svg)](https://godoc.org/github.com/codesoap/atto)

atto is a tiny Nano wallet, which focuses on ease of use through
simplicity.
simplicity. Included is a rudimentary Go library to interact with Nano
nodes.

Disclaimer: I am no cryptographer and atto has not been audited. I
cannot guarantee that atto is free of security compromising bugs.
@@ -89,12 +92,12 @@ the same seed. By default the account with index 0 is chosen.
```

# Technical details
atto is written with less than 1000 lines of code and uses minimal
external dependencies. This makes it easy to audit the code yourself
and ensure, that it does nothing you wouldn't want it to do.
atto is written with ca. 1000 lines of code and uses minimal external
dependencies. This makes it easy to audit the code yourself and ensure,
that it does nothing you wouldn't want it to do.

To change some defaults, like the node to use, take a look at
`config.go`.
`cmd/atto/config.go`.

Signatures are created without the help of a node, to avoid your seed or
private keys being stolen by a node operator. The received account info

+ 60
- 49
account.go View File

@@ -1,65 +1,43 @@
package main
package atto

import (
"bufio"
"encoding/json"
"fmt"
"math/big"
"os"
"strings"

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

var errAccountNotFound = fmt.Errorf("account has not yet been opened")
// ErrAccountNotFound is used when an account could not be found by the
// queried node.
var ErrAccountNotFound = fmt.Errorf("account has not yet been opened")

type account struct {
privateKey *big.Int
publicKey *big.Int
address string
}
// ErrAccountManipulated is used when it seems like an account has been
// manipulated. This probably means someone is trying to steal funds.
var ErrAccountManipulated = fmt.Errorf("the received account info has been manipulated")

type accountInfo struct {
Error string `json:"error"`
Frontier string `json:"frontier"`
Representative string `json:"representative"`
Balance string `json:"balance"`
// Account holds the keys and address of a Nano account.
type Account struct {
PrivateKey *big.Int
PublicKey *big.Int
Address string
}

type blockInfo struct {
Error string `json:"error"`
Contents block `json:"contents"`
Contents Block `json:"contents"`
}

// ownAccount initializes the own account using the seed provided via
// standard input and accountIndexFlag.
func ownAccount() (a account, err error) {
seed, err := getSeed()
if err != nil {
return
}
a.privateKey = getPrivateKey(seed, uint32(accountIndexFlag))
a.publicKey = derivePublicKey(a.privateKey)
a.address, err = getAddress(a.publicKey)
// NewAccount creates a new Account and populates all its fields.
func NewAccount(seed *big.Int, index uint32) (a Account, err error) {
a.PrivateKey = getPrivateKey(seed, index)
a.PublicKey = derivePublicKey(a.PrivateKey)
a.Address, err = getAddress(a.PublicKey)
return
}

// 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 nil, fmt.Errorf("could not parse seed")
}
return seed, nil
}

func getPrivateKey(seed *big.Int, index uint32) *big.Int {
seedBytes := bigIntToBytes(seed, 32)
indexBytes := bigIntToBytes(big.NewInt(int64(index)), 4)
@@ -95,13 +73,20 @@ func getAddress(publicKey *big.Int) (string, error) {
return address, nil
}

func (a account) getInfo() (info accountInfo, err error) {
// FetchAccountInfo fetches the AccountInfo of Account from the given
// node.
//
// It is also verified, that the retreived AccountInfo is valid by
// doing a block_info RPC for the frontier, verifying the signature
// and ensuring that no fields have been changed in the account_info
// response.
func (a Account) FetchAccountInfo(node string) (info AccountInfo, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "account_info",`+
`"account": "%s",`+
`"representative": "true"`+
`}`, a.address)
responseBytes, err := doRPC(requestBody)
`}`, a.Address)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return
}
@@ -111,24 +96,26 @@ func (a account) getInfo() (info accountInfo, err error) {
// Need to check info.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if info.Error == "Account not found" {
err = errAccountNotFound
err = ErrAccountNotFound
} else if info.Error != "" {
err = fmt.Errorf("could not fetch account info: %s", info.Error)
} else {
err = a.verifyInfo(info)
info.PublicKey = a.PublicKey
info.Address = a.Address
err = a.verifyInfo(info, node)
}
return
}

// verifyInfo gets the frontier block of info, ensures that Hash,
// Representative and Balance match and verifies it's signature.
func (a account) verifyInfo(info accountInfo) error {
func (a Account) verifyInfo(info AccountInfo, node string) error {
requestBody := fmt.Sprintf(`{`+
`"action": "block_info",`+
`"json_block": "true",`+
`"hash": "%s"`+
`}`, info.Frontier)
responseBytes, err := doRPC(requestBody)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return err
}
@@ -139,12 +126,36 @@ func (a account) verifyInfo(info accountInfo) error {
if info.Error != "" {
return fmt.Errorf("could not get block info: %s", info.Error)
}
if err = block.Contents.hash(info.PublicKey); err != nil {
return err
}
if err = block.Contents.verifySignature(a); err == errInvalidSignature ||
info.Frontier != block.Contents.Hash ||
info.Representative != block.Contents.Representative ||
info.Balance != block.Contents.Balance {
return fmt.Errorf("the received account info has been manipulated; " +
"change your node immediately!")
return ErrAccountManipulated
}
return err
}

// FetchPending fetches all unreceived blocks of Account from node.
func (a Account) FetchPending(node string) (sends []Pending, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "pending", `+
`"account": "%s", `+
`"include_only_confirmed": "true", `+
`"source": "true"`+
`}`, a.Address)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return
}
var pending internalPending
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 internalPendingToPending(pending), err
}

+ 133
- 0
account_info.go View File

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

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

// AccountInfo holds the relevant data returned by an account_info RPC
// and the public key and address of the account.
type AccountInfo struct {
// Ignore this field. It only exists because of
// https://github.com/nanocurrency/nano-node/issues/1782.
Error string `json:"error"`

Frontier string `json:"frontier"`
Representative string `json:"representative"`
Balance string `json:"balance"`

PublicKey *big.Int `json:"-"`
Address string `json:"-"`
}

// Send creates a send block, which is hashed but missing the signature
// and work. The Frontier and Balance of the AccountInfo will be
// updated. The amount is interpreted as Nano, not raw!
func (i *AccountInfo) Send(toAddr, amount string) (Block, error) {
balance, err := getBalanceAfterSend(i.Balance, amount)
if err != nil {
return Block{}, err
}
recipientNumber, err := getPublicKeyFromAddress(toAddr)
if err != nil {
return Block{}, err
}
recipientBytes := bigIntToBytes(recipientNumber, 32)
block := Block{
Type: "state",
SubType: "send",
Account: i.Address,
Previous: i.Frontier,
Representative: i.Representative,
Balance: balance.String(),
Link: fmt.Sprintf("%064X", recipientBytes),
}
err = block.hash(i.PublicKey)
i.Frontier = block.Hash
i.Balance = block.Balance
return block, err
}

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
}
amountRaw, err := nanoStringToRaw(amount)
if err != nil {
return nil, err
}
return balance.Sub(balance, amountRaw), nil
}

func nanoStringToRaw(amountString string) (*big.Int, error) {
pattern := `^([0-9]+|[0-9]*\.[0-9]{1,30})$`
amountOk, err := regexp.MatchString(pattern, amountString)
if !amountOk {
return nil, fmt.Errorf("'%s' is no legal amountString", amountString)
} else if err != nil {
return nil, err
}
missingZerosUntilRaw := 30
if i := strings.Index(amountString, "."); i > -1 {
missingZerosUntilRaw -= len(amountString) - i - 1
amountString = strings.Replace(amountString, ".", "", 1)
}
amountString += strings.Repeat("0", missingZerosUntilRaw)
amount, ok := big.NewInt(0).SetString(amountString, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an interger", amountString)
return nil, err
}
return amount, nil
}

// Change creates a change block, which is hashed but missing the
// signature and work. The Frontier of the AccountInfo will be updated.
func (i *AccountInfo) Change(representative string) (Block, error) {
block := Block{
Type: "state",
SubType: "change",
Account: i.Address,
Previous: i.Frontier,
Representative: representative,
Balance: i.Balance,
Link: "0000000000000000000000000000000000000000000000000000000000000000",
}
err := block.hash(i.PublicKey)
i.Frontier = block.Hash
return block, err
}

// Receive creates a receive block, which is hashed but missing the
// signature and work. The Frontier and Balance of the AccountInfo will
// be updated.
func (i *AccountInfo) Receive(pending Pending) (Block, error) {
updatedBalance, ok := big.NewInt(0).SetString(i.Balance, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an integer", i.Balance)
return Block{}, err
}
amount, ok := big.NewInt(0).SetString(pending.Amount, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an integer", pending.Amount)
return Block{}, err
}
updatedBalance.Add(updatedBalance, amount)
block := Block{
Type: "state",
SubType: "receive",
Account: i.Address,
Previous: i.Frontier,
Representative: i.Representative,
Balance: updatedBalance.String(),
Link: pending.Hash,
}
err := block.hash(i.PublicKey)
i.Frontier = block.Hash
i.Balance = block.Balance
return block, err
}

+ 0
- 23
address.go View File

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

import (
"fmt"
"math/big"
)

func printAddress() error {
account, err := ownAccount()
if err == nil {
fmt.Println(account.address)
}
return err
}

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)
}

+ 0
- 128
balance.go View File

@@ -1,128 +0,0 @@
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 {
account, err := ownAccount()
if err != nil {
return err
}
info, err := account.getInfo()
if err == errAccountNotFound {
// This info is needed to create the first block:
info.Frontier = "0000000000000000000000000000000000000000000000000000000000000000"
info.Representative = defaultRepresentative
info.Balance = "0"
} else if err != nil {
return err
}
updatedBalance, err := account.receivePendingSends(info)
if err != nil {
return err
}
fmt.Println(rawToNanoString(updatedBalance))
return nil
}

func (a account) receivePendingSends(info accountInfo) (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
}
sends, err := a.getPendingSends()
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.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: a.address,
Previous: previousBlock,
Representative: info.Representative,
Balance: updatedBalance.String(),
Link: blockHash,
}
if err = block.sign(a); err != nil {
return
}
if err = block.addWork(receiveWorkThreshold, a); err != nil {
return
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "receive",
Block: block,
}
if err = doProcessRPC(process); err != nil {
return
}

fmt.Fprintln(os.Stderr, "done")
previousBlock = block.Hash // Hash was computed during signing.
}
return
}

func (a account) getPendingSends() (sends pendingBlocks, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "pending", `+
`"account": "%s", `+
`"include_only_confirmed": "true", `+
`"source": "true"`+
`}`, a.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
}

+ 66
- 51
block.go View File

@@ -1,4 +1,4 @@
package main
package atto

import (
"encoding/hex"
@@ -11,8 +11,18 @@ import (

var errInvalidSignature = fmt.Errorf("invalid block signature")

type block struct {
// ErrSignatureMissing is used when the Signature of a Block is missing
// but required for the attempted operation.
var ErrSignatureMissing = fmt.Errorf("signature is missing")

// ErrWorkMissing is used when the Work of a Block is missing but
// required for the attempted operation.
var ErrWorkMissing = fmt.Errorf("work is missing")

// Block represents a block in the block chain of an account.
type Block struct {
Type string `json:"type"`
SubType string `json:"-"`
Account string `json:"account"`
Previous string `json:"previous"`
Representative string `json:"representative"`
@@ -29,11 +39,9 @@ type workGenerateResponse struct {
Work string `json:"work"`
}

func (b *block) sign(a account) error {
if err := b.addHashIfUnhashed(a); err != nil {
return err
}
signature, err := sign(a, b.HashBytes)
// Sign computes and sets the Signature of b.
func (b *Block) Sign(publicKey, privateKey *big.Int) error {
signature, err := sign(publicKey, privateKey, b.HashBytes)
if err != nil {
return err
}
@@ -41,94 +49,101 @@ func (b *block) sign(a account) error {
return nil
}

func (b *block) verifySignature(a account) (err error) {
if err = b.addHashIfUnhashed(a); err != nil {
return
}
func (b *Block) verifySignature(a Account) (err error) {
sig, ok := big.NewInt(0).SetString(b.Signature, 16)
if !ok {
return fmt.Errorf("cannot parse '%s' as an integer", b.Signature)
}
if !isValidSignature(a, b.HashBytes, bigIntToBytes(sig, 64)) {
if !isValidSignature(a.PublicKey, b.HashBytes, bigIntToBytes(sig, 64)) {
err = errInvalidSignature
}
return
}

func (b *block) addHashIfUnhashed(a account) error {
if b.Hash == "" || len(b.HashBytes) == 0 {
hashBytes, err := b.hash(a)
if err != nil {
return err
}
b.HashBytes = hashBytes
b.Hash = fmt.Sprintf("%064X", b.HashBytes)
// FetchWork uses the generate_work RPC on node to fetch and then set
// the Work of b.
func (b *Block) FetchWork(workThreshold uint64, publicKey *big.Int, node string) error {
var hash string
if b.Previous == "0000000000000000000000000000000000000000000000000000000000000000" {
hash = fmt.Sprintf("%064X", bigIntToBytes(publicKey, 32))
} else {
hash = b.Previous
}
requestBody := fmt.Sprintf(`{`+
`"action": "work_generate",`+
`"hash": "%s",`+
`"difficulty": "%016x"`+
`}`, string(hash), workThreshold)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return err
}
var response workGenerateResponse
if err = json.Unmarshal(responseBytes, &response); err != nil {
return err
}
// Need to check response.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if response.Error != "" {
return fmt.Errorf("could not get work for block: %s", response.Error)
}
b.Work = response.Work
return nil
}

func (b *block) hash(a account) ([]byte, error) {
func (b *Block) hash(publicKey *big.Int) error {
// See https://nanoo.tools/block for a reference.

msg := make([]byte, 176, 176)

msg[31] = 0x6 // block preamble

copy(msg[32:64], bigIntToBytes(a.publicKey, 32))
copy(msg[32:64], bigIntToBytes(publicKey, 32))

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

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

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

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

hash := blake2b.Sum256(msg)
return hash[:], nil
b.HashBytes = hash[:]
b.Hash = fmt.Sprintf("%064X", b.HashBytes)
return nil
}

func (b *block) addWork(workThreshold uint64, a account) error {
var hash string
if b.Previous == "0000000000000000000000000000000000000000000000000000000000000000" {
hash = fmt.Sprintf("%064X", bigIntToBytes(a.publicKey, 32))
} else {
hash = b.Previous
}
requestBody := fmt.Sprintf(`{`+
`"action": "work_generate",`+
`"hash": "%s",`+
`"difficulty": "%016x"`+
`}`, string(hash), workThreshold)
responseBytes, err := doRPC(requestBody)
if err != nil {
return err
// Submit submits the Block to the given node. Work and Signature of b
// must be populated beforehand.
func (b Block) Submit(node string) error {
if b.Work == "" {
return ErrWorkMissing
}
var response workGenerateResponse
if err = json.Unmarshal(responseBytes, &response); err != nil {
return err
if b.Signature == "" {
return ErrSignatureMissing
}
// Need to check response.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if response.Error != "" {
return fmt.Errorf("could not get work for block: %s", response.Error)
process := process{
Action: "process",
JsonBlock: "true",
SubType: b.SubType,
Block: b,
}
b.Work = response.Work
return nil
return doProcessRPC(process, node)
}

+ 0
- 51
change.go View File

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

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

func changeRepresentative() error {
account, err := ownAccount()
if err != nil {
return err
}
info, err := account.getInfo()
if err != nil {
return err
}
representative := flag.Arg(1)
fmt.Fprintf(os.Stderr, "Creating change block... ")
err = account.changeRepresentativeOfAccount(info, representative)
if err != nil {
fmt.Fprintln(os.Stderr, "")
return err
}
fmt.Fprintln(os.Stderr, "done")
return nil
}

func (a account) changeRepresentativeOfAccount(info accountInfo, representative string) error {
block := block{
Type: "state",
Account: a.address,
Previous: info.Frontier,
Representative: representative,
Balance: info.Balance,
Link: "0000000000000000000000000000000000000000000000000000000000000000",
}
if err := block.sign(a); err != nil {
return err
}
if err := block.addWork(changeWorkThreshold, a); err != nil {
return err
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "change",
Block: block,
}
return doProcessRPC(process)
}

+ 0
- 17
config.go View File

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

var (
// The node needs to support the work_generate action.
// See e.g. https://publicnodes.somenano.com to find public nodes or
// set up your own node to use. Here are some public alternatives:
// - https://proxy.nanos.cc/proxy
// - https://mynano.ninja/api/node
// - https://rainstorm.city/api
nodeUrl = "https://proxy.powernode.cc/proxy"

defaultRepresentative = "nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz"

sendWorkThreshold uint64 = 0xfffffff800000000
changeWorkThreshold uint64 = 0xfffffff800000000
receiveWorkThreshold uint64 = 0xfffffe0000000000
)

+ 8
- 6
ed25519.go View File

@@ -1,11 +1,13 @@
package main
package atto

import (
"math/big"

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

func sign(a account, msg []byte) ([]byte, error) {
func sign(publicKey, privateKey *big.Int, msg []byte) ([]byte, error) {
// This implementation based on the one from github.com/iotaledger/iota.go.

signature := make([]byte, 64, 64)
@@ -14,7 +16,7 @@ func sign(a account, msg []byte) ([]byte, error) {
if err != nil {
return signature, err
}
h.Write(bigIntToBytes(a.privateKey, 32))
h.Write(bigIntToBytes(privateKey, 32))

var digest1, messageDigest, hramDigest [64]byte
h.Sum(digest1[:0])
@@ -33,7 +35,7 @@ func sign(a account, msg []byte) ([]byte, error) {

h.Reset()
h.Write(encodedR[:])
h.Write(bigIntToBytes(a.publicKey, 32))
h.Write(bigIntToBytes(publicKey, 32))
h.Write(msg)
h.Sum(hramDigest[:0])

@@ -46,10 +48,10 @@ func sign(a account, msg []byte) ([]byte, error) {
return signature, nil
}

func isValidSignature(a account, msg, sig []byte) bool {
func isValidSignature(publicKey *big.Int, msg, sig []byte) bool {
// This implementation based on the one from github.com/iotaledger/iota.go.

publicKeyBytes := bigIntToBytes(a.publicKey, 32)
publicKeyBytes := bigIntToBytes(publicKey, 32)

// ZIP215: this works because SetBytes does not check that encodings are canonical
A, err := new(edwards25519.Point).SetBytes(publicKeyBytes)

+ 0
- 91
main.go View File

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

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

var usage = `Usage:
atto -v
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] [-y] s[end] AMOUNT RECEIVER

If the -v flag is provided, atto will print its version number.

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

The address, balance, representative and send subcommands 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 send subcommand also expects manual confirmation of the transaction,
unless the -y flag is given.

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
var yFlag bool

func init() {
var vFlag bool
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
flag.UintVar(&accountIndexFlag, "a", 0, "")
flag.BoolVar(&yFlag, "y", false, "")
flag.BoolVar(&vFlag, "v", false, "")
flag.Parse()
if vFlag {
fmt.Println("1.2.0")
os.Exit(0)
}
if flag.NArg() < 1 {
flag.Usage()
os.Exit(1)
}
var ok bool
switch flag.Arg(0)[:1] {
case "n", "a", "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)
}
}

+ 0
- 15
new.go View File

@@ -1,15 +0,0 @@
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
}

+ 47
- 0
pending.go View File

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

import (
"encoding/json"
)

// Pending represents a block that is waiting to be received.
type Pending struct {
Hash string
Amount string
Source string
}

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

type pendingBlocks map[string]pendingBlock

// 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]pendingBlock
err := json.Unmarshal(in, &raw)
*b = pendingBlocks(raw)
return err
}

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

func internalPendingToPending(internalPending internalPending) []Pending {
pendings := make([]Pending, 0)
for hash, source := range internalPending.Blocks {
pending := Pending{hash, source.Amount, source.Source}
pendings = append(pendings, pending)
}
return pendings
}

+ 5
- 5
process.go View File

@@ -1,4 +1,4 @@
package main
package atto

import (
"encoding/json"
@@ -8,21 +8,21 @@ import (
type process struct {
Action string `json:"action"`
JsonBlock string `json:"json_block"`
Subtype string `json:"subtype"`
Block block `json:"block"`
SubType string `json:"subtype"`
Block Block `json:"block"`
}

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

func doProcessRPC(process process) error {
func doProcessRPC(process process, node string) error {
var requestBody, responseBytes []byte
requestBody, err := json.Marshal(process)
if err != nil {
return err
}
responseBytes, err = doRPC(string(requestBody))
responseBytes, err = doRPC(string(requestBody), node)
if err != nil {
return err
}

+ 0
- 130
send.go View File

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

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

func sendFunds() error {
amount := flag.Arg(1)
recipient := flag.Arg(2)
account, err := ownAccount()
if err != nil {
return err
}
if err = letUserVerifySend(amount, recipient); err != nil {
return err
}
info, err := account.getInfo()
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Creating send block... ")
err = account.sendFundsToAccount(info, amount, recipient)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "done")
return nil
}

func letUserVerifySend(amount, recipient string) (err error) {
if !yFlag {
fmt.Printf("Send %s NANO to %s? [y/N]: ", amount, recipient)

// Explicitly openning /dev/tty or CONIN$ ensures function, even if
// the standard input is not a terminal.
var tty *os.File
if runtime.GOOS == "windows" {
tty, err = os.Open("CONIN$")
} else {
tty, err = os.Open("/dev/tty")
}
if err != nil {
msg := "could not open terminal for confirmation input: %v"
return fmt.Errorf(msg, err)
}
defer tty.Close()

var confirmation string
fmt.Fscanln(tty, &confirmation)
if confirmation != "y" && confirmation != "Y" {
fmt.Fprintln(os.Stderr, "Send aborted.")
os.Exit(0)
}
}
return
}

func (a account) sendFundsToAccount(info accountInfo, amount, recipient string) error {
balance, err := getBalanceAfterSend(info.Balance, amount)
if err != nil {
return err
}
recipientNumber, err := getPublicKeyFromAddress(recipient)
if err != nil {
return err
}
recipientBytes := bigIntToBytes(recipientNumber, 32)
block := block{
Type: "state",
Account: a.address,
Previous: info.Frontier,
Representative: info.Representative,
Balance: balance.String(),
Link: fmt.Sprintf("%064X", recipientBytes),
}
if err = block.sign(a); err != nil {
return err
}
if err = block.addWork(sendWorkThreshold, a); err != nil {
return err
}
process := process{
Action: "process",
JsonBlock: "true",
Subtype: "send",
Block: block,
}
return doProcessRPC(process)
}

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
}
amountRaw, err := nanoStringToRaw(amount)
if err != nil {
return nil, err
}
return balance.Sub(balance, amountRaw), nil
}

func nanoStringToRaw(amountString string) (*big.Int, error) {
pattern := `^([0-9]+|[0-9]*\.[0-9]{1,30})$`
amountOk, err := regexp.MatchString(pattern, amountString)
if !amountOk {
return nil, fmt.Errorf("'%s' is no legal amountString", amountString)
} else if err != nil {
return nil, err
}
missingZerosUntilRaw := 30
if i := strings.Index(amountString, "."); i > -1 {
missingZerosUntilRaw -= len(amountString) - i - 1
amountString = strings.Replace(amountString, ".", "", 1)
}
amountString += strings.Repeat("0", missingZerosUntilRaw)
amount, ok := big.NewInt(0).SetString(amountString, 10)
if !ok {
err := fmt.Errorf("cannot parse '%s' as an interger", amountString)
return nil, err
}
return amount, nil
}

+ 10
- 7
util.go View File

@@ -1,4 +1,4 @@
package main
package atto

import (
"fmt"
@@ -83,8 +83,8 @@ func revertBytes(in []byte) []byte {
return in
}

func doRPC(requestBody string) (responseBytes []byte, err error) {
resp, err := http.Post(nodeUrl, "application/json", strings.NewReader(requestBody))
func doRPC(requestBody, node string) (responseBytes []byte, err error) {
resp, err := http.Post(node, "application/json", strings.NewReader(requestBody))
if err != nil {
return
}
@@ -96,8 +96,11 @@ func doRPC(requestBody string) (responseBytes []byte, err error) {
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)
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)
}

Loading…
Cancel
Save