-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathaccount.go
196 lines (180 loc) · 5.83 KB
/
account.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package atto
import (
"encoding/json"
"fmt"
"math/big"
"strings"
"filippo.io/edwards25519"
"golang.org/x/crypto/blake2b"
)
// ErrAccountNotFound is used when an account could not be found by the
// queried node.
var ErrAccountNotFound = fmt.Errorf("account has not yet been opened")
// 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")
// Account holds the public key and address of a Nano account.
type Account struct {
PublicKey *big.Int
Address string
}
type blockInfo struct {
Error string `json:"error"`
Contents Block `json:"contents"`
}
// NewAccount creates a new Account and populates both its fields.
func NewAccount(privateKey *big.Int) (a Account, err error) {
a.PublicKey = derivePublicKey(privateKey)
a.Address, err = getAddress(a.PublicKey)
return
}
// NewAccountFromAddress creates a new Account and populates both its
// fields.
func NewAccountFromAddress(address string) (a Account, err error) {
a.Address = address
a.PublicKey, err = getPublicKeyFromAddress(address)
return
}
func derivePublicKey(privateKey *big.Int) *big.Int {
hashBytes := blake2b.Sum512(bigIntToBytes(privateKey, 32))
scalar, err := edwards25519.NewScalar().SetBytesWithClamping(hashBytes[:32])
if err != nil {
panic(err)
}
publicKeyBytes := edwards25519.NewIdentityPoint().ScalarBaseMult(scalar).Bytes()
return big.NewInt(0).SetBytes(publicKeyBytes)
}
func getAddress(publicKey *big.Int) (string, error) {
base32PublicKey := base32Encode(publicKey)
hasher, err := blake2b.New(5, nil)
if err != nil {
return "", err
}
publicKeyBytes := bigIntToBytes(publicKey, 32)
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
}
// 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.
//
// May return ErrAccountNotFound or ErrAccountManipulated.
//
// If ErrAccountNotFound is returned, FirstReceive can be used to
// create a first Block and AccountInfo and create the account by then
// submitting this Block.
func (a Account) FetchAccountInfo(node string) (i AccountInfo, err error) {
requestBody := fmt.Sprintf(`{`+
`"action": "account_info",`+
`"account": "%s",`+
`"representative": "true"`+
`}`, a.Address)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return
}
if err = json.Unmarshal(responseBytes, &i); err != nil {
return
}
// Need to check i.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if i.Error == "Account not found" {
err = ErrAccountNotFound
} else if i.Error != "" {
err = fmt.Errorf("could not fetch account info: %s", i.Error)
} else {
i.PublicKey = a.PublicKey
i.Address = a.Address
err = a.verifyInfo(i, 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, node string) error {
requestBody := fmt.Sprintf(`{`+
`"action": "block_info",`+
`"json_block": "true",`+
`"hash": "%s"`+
`}`, info.Frontier)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return err
}
var block blockInfo
if err = json.Unmarshal(responseBytes, &block); err != nil {
return err
}
if info.Error != "" {
return fmt.Errorf("could not get block info: %s", info.Error)
}
hash, err := block.Contents.Hash()
if err != nil {
return err
}
if err = block.Contents.verifySignature(a); err == errInvalidSignature ||
info.Frontier != hash ||
info.Representative != block.Contents.Representative ||
info.Balance != block.Contents.Balance {
return ErrAccountManipulated
}
return err
}
// FetchReceivable fetches all unreceived blocks of Account from node.
func (a Account) FetchReceivable(node string) ([]Receivable, error) {
requestBody := fmt.Sprintf(`{`+
`"action": "receivable", `+
`"account": "%s", `+
`"include_only_confirmed": "true", `+
`"source": "true"`+
`}`, a.Address)
responseBytes, err := doRPC(requestBody, node)
if err != nil {
return nil, err
}
var receivable internalReceivable
err = json.Unmarshal(responseBytes, &receivable)
// Need to check receivable.Error because of
// https://github.com/nanocurrency/nano-node/issues/1782.
if err == nil && receivable.Error != "" {
err = fmt.Errorf("could not fetch unreceived sends: %s", receivable.Error)
}
return internalReceivableToReceivable(receivable), err
}
// FirstReceive creates the first receive block of an account. The block
// will still be missing its signature and work. FirstReceive will also
// return AccountInfo, which can be used to create further blocks.
func (a Account) FirstReceive(receivable Receivable, representative string) (AccountInfo, Block, error) {
block := Block{
Type: "state",
SubType: SubTypeReceive,
Account: a.Address,
Previous: "0000000000000000000000000000000000000000000000000000000000000000",
Representative: representative,
Balance: receivable.Amount,
Link: receivable.Hash,
}
hash, err := block.Hash()
if err != nil {
return AccountInfo{}, Block{}, err
}
info := AccountInfo{
Frontier: hash,
Representative: block.Representative,
Balance: block.Balance,
PublicKey: a.PublicKey,
Address: a.Address,
}
return info, block, err
}