forked from textileio/go-textile
/
textile.go
458 lines (393 loc) · 12.3 KB
/
textile.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
package main
import (
"errors"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"github.com/textileio/textile-go/cmd"
"github.com/textileio/textile-go/core"
"github.com/textileio/textile-go/gateway"
"github.com/textileio/textile-go/keypair"
"github.com/textileio/textile-go/wallet"
)
type ipfsOptions struct {
ServerMode bool `long:"server" description:"Apply IPFS server profile."`
SwarmPorts string `long:"swarm-ports" description:"Set the swarm ports (TCP,WS). Random ports are chosen by default."`
}
type logOptions struct {
Level string `short:"l" long:"log-level" description:"Set the logging level [debug, info, notice, warning, error, critical]." default:"error"`
NoFiles bool `short:"n" long:"no-log-files" description:"Write logs to stdout instead of rolling files."`
}
type addressOptions struct {
ApiBindAddr string `short:"a" long:"api-bind-addr" description:"Set the local API address." default:"127.0.0.1:40600"`
CafeApiBindAddr string `short:"c" long:"cafe-bind-addr" description:"Set the cafe REST API address." default:"127.0.0.1:40601"`
GatewayBindAddr string `short:"g" long:"gateway-bind-addr" description:"Set the IPFS gateway address." default:"127.0.0.1:5050"`
}
type cafeOptions struct {
PublicAddr string `long:"public-addr" description:"Required with --cafe-open on a server with a public IP address, e.g., https://<IP_ADDRESS>'"`
Open bool `long:"cafe-open" description:"Opens the p2p Cafe Service for other peers."`
}
type options struct{}
type walletCmd struct {
Init walletInitCmd `command:"init" description:"Initialize a new wallet"`
Accounts walletAccountsCmd `command:"accounts" description:"Show derived accounts"`
}
type walletInitCmd struct {
WordCount int `short:"w" long:"word-count" description:"Number of mnemonic recovery phrase words: 12,15,18,21,24." default:"12"`
Password string `short:"p" long:"password" description:"Mnemonic recovery phrase password (omit if none)."`
}
func (x *walletInitCmd) Usage() string {
return `
Initializes a new account wallet backed by a mnemonic recovery phrase.
`
}
type walletAccountsCmd struct {
Password string `short:"p" long:"password" description:"Mnemonic recovery phrase password (omit if none)."`
Depth int `short:"d" long:"depth" description:"Number of accounts to show." default:"1"`
Offset int `short:"o" long:"offset" description:"Account depth to start from." default:"0"`
}
func (x *walletAccountsCmd) Usage() string {
return `
Shows the derived accounts (address/seed pairs) in a wallet.
`
}
type versionCmd struct{}
type initCmd struct {
AccountSeed string `required:"true" short:"s" long:"seed" description:"Account seed (run 'wallet' command to generate new seeds)."`
PinCode string `short:"p" long:"pin-code" description:"Specify a pin code for datastore encryption."`
RepoPath string `short:"r" long:"repo-dir" description:"Specify a custom repository path."`
Addresses addressOptions `group:"Address Options"`
CafeOptions cafeOptions `group:"Cafe Options"`
IPFS ipfsOptions `group:"IPFS Options"`
Logs logOptions `group:"Log Options"`
}
type migrateCmd struct {
RepoPath string `short:"r" long:"repo-dir" description:"Specify a custom repository path."`
PinCode string `short:"p" long:"pin-code" description:"Specify the pin code for datastore encryption (omit of none was used during init)."`
}
type daemonCmd struct {
PinCode string `short:"p" long:"pin-code" description:"Specify the pin code for datastore encryption (omit of none was used during init)."`
RepoPath string `short:"r" long:"repo-dir" description:"Specify a custom repository path."`
Logs []string `short:"l" long:"logs" description:"Control subcommand log level. e.g., --logs=\"tex-core: debug\" Can be used multiple times."`
}
type commandsCmd struct {
}
var node *core.Textile
var parser = flags.NewParser(&options{}, flags.Default)
func init() {
// add main commands
parser.AddCommand("version",
"Print version and exit",
"Print the current version and exit.",
&versionCmd{})
parser.AddCommand("wallet",
"Manage or create an account wallet",
"Initialize a new wallet, or view accounts from an existing wallet.",
&walletCmd{})
parser.AddCommand("init",
"Init the node repo and exit",
"Initialize the node repository and exit.",
&initCmd{})
parser.AddCommand("migrate",
"Migrate the node repo and exit",
"Migrate the node repository and exit.",
&migrateCmd{})
parser.AddCommand("daemon",
"Start the daemon",
"Start a node daemon session.",
&daemonCmd{})
parser.AddCommand("commands",
"List available commands",
"List all available textile commands.",
&commandsCmd{})
// add cmd commands
for _, c := range cmd.Cmds() {
parser.AddCommand(c.Name(), c.Short(), c.Long(), c)
}
}
func main() {
parser.Parse()
}
func (x *commandsCmd) Execute(args []string) error {
for _, c := range parser.Commands() {
if len(c.Commands()) == 0 {
fmt.Println(fmt.Sprintf("textile %s", c.Name))
}
for _, sub := range c.Commands() {
fmt.Println(fmt.Sprintf("textile %s %s", c.Name, sub.Name))
}
}
return nil
}
func (x *walletInitCmd) Execute(args []string) error {
wcount, err := wallet.NewWordCount(x.WordCount)
if err != nil {
return err
}
w, err := wallet.NewWallet(wcount.EntropySize())
if err != nil {
return err
}
fmt.Println(strings.Repeat("-", len(w.RecoveryPhrase)+4))
fmt.Println("| " + w.RecoveryPhrase + " |")
fmt.Println(strings.Repeat("-", len(w.RecoveryPhrase)+4))
fmt.Println("WARNING! Store these words above in a safe place!")
fmt.Println("WARNING! If you lose your words, you will lose access to data in all derived accounts!")
fmt.Println("WARNING! Anyone who has access to these words can access your wallet accounts!")
fmt.Println("")
fmt.Println("Use: `wallet accounts` command to inspect more accounts.")
fmt.Println("")
// show first account
kp, err := w.AccountAt(0, x.Password)
if err != nil {
return err
}
fmt.Println("--- ACCOUNT 0 ---")
fmt.Println(kp.Address())
fmt.Println(kp.Seed())
return nil
}
func (x *walletAccountsCmd) Execute(args []string) error {
if len(args) == 0 {
return errors.New("missing recovery phrase")
}
if x.Depth < 1 || x.Depth > 100 {
return errors.New("depth must be greater than 0 and less than 100")
}
if x.Offset < 0 || x.Offset > x.Depth {
return errors.New("offset must be greater than 0 and less than depth")
}
wall := wallet.NewWalletFromRecoveryPhrase(args[0])
for i := x.Offset; i < x.Offset+x.Depth; i++ {
kp, err := wall.AccountAt(i, x.Password)
if err != nil {
return err
}
fmt.Println(fmt.Sprintf("--- ACCOUNT %d ---", i))
fmt.Println(kp.Address())
fmt.Println(kp.Seed())
}
return nil
}
func (x *versionCmd) Execute(args []string) error {
fmt.Println(core.Version)
return nil
}
func (x *initCmd) Execute(args []string) error {
// build keypair from provided seed
kp, err := keypair.Parse(x.AccountSeed)
if err != nil {
return errors.New(fmt.Sprintf("parse account seed failed: %s", err))
}
accnt, ok := kp.(*keypair.Full)
if !ok {
return keypair.ErrInvalidKey
}
repoPath, err := getRepoPath(x.RepoPath)
if err != nil {
return err
}
config := core.InitConfig{
Account: accnt,
PinCode: x.PinCode,
RepoPath: repoPath,
SwarmPorts: x.IPFS.SwarmPorts,
ApiAddr: x.Addresses.ApiBindAddr,
CafeApiAddr: x.Addresses.CafeApiBindAddr,
GatewayAddr: x.Addresses.GatewayBindAddr,
IsMobile: false,
IsServer: x.IPFS.ServerMode,
LogToDisk: !x.Logs.NoFiles,
CafeOpen: x.CafeOptions.Open,
CafePublicAddr: x.CafeOptions.PublicAddr,
}
if err := core.InitRepo(config); err != nil {
return errors.New(fmt.Sprintf("initialize failed: %s", err))
}
fmt.Printf("Initialized account with address %s\n", accnt.Address())
return nil
}
func (x *migrateCmd) Execute(args []string) error {
repoPath, err := getRepoPath(x.RepoPath)
if err != nil {
return err
}
if err := core.MigrateRepo(core.MigrateConfig{
PinCode: x.PinCode,
RepoPath: repoPath,
}); err != nil {
return errors.New(fmt.Sprintf("migrate repo: %s", err))
}
fmt.Println("Repo was successfully migrated")
return nil
}
func (x *daemonCmd) Execute(args []string) error {
logLevels := make(map[string]string)
for _, l := range x.Logs {
split := strings.SplitN(l, ":", 2)
key := strings.TrimSpace(split[0])
value := strings.ToUpper(strings.TrimSpace(split[1]))
logLevels[key] = value
}
if err := buildNode(x.PinCode, x.RepoPath, logLevels); err != nil {
return err
}
printSplash()
// handle interrupt
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
fmt.Println("Interrupted")
fmt.Printf("Shutting down...")
if err := stopNode(); err != nil && err != core.ErrStopped {
fmt.Println(err.Error())
} else {
fmt.Print("done\n")
}
os.Exit(1)
return nil
}
func getRepoPath(repoPath string) (string, error) {
if len(repoPath) == 0 {
// get homedir
home, err := homedir.Dir()
if err != nil {
return "", errors.New(fmt.Sprintf("get homedir failed: %s", err))
}
// ensure app folder is created
appDir := filepath.Join(home, ".textile")
if err := os.MkdirAll(appDir, 0755); err != nil {
return "", errors.New(fmt.Sprintf("create repo directory failed: %s", err))
}
repoPath = filepath.Join(appDir, "repo")
}
return repoPath, nil
}
func buildNode(pinCode string, repoPath string, logLevels map[string]string) error {
repoPathf, err := getRepoPath(repoPath)
if err != nil {
return err
}
node, err = core.NewTextile(core.RunConfig{
PinCode: pinCode,
RepoPath: repoPathf,
LogLevels: logLevels,
})
if err != nil {
return errors.New(fmt.Sprintf("create node failed: %s", err))
}
gateway.Host = &gateway.Gateway{
Node: node,
}
if err := startNode(); err != nil {
return errors.New(fmt.Sprintf("start node failed: %s", err))
}
return nil
}
func startNode() error {
listener := node.GetThreadUpdateListener()
if err := node.Start(); err != nil {
return err
}
// subscribe to wallet updates
go func() {
for {
select {
case update, ok := <-node.UpdateCh():
if !ok {
return
}
switch update.Type {
case core.ThreadAdded:
break
case core.ThreadRemoved:
break
case core.AccountPeerAdded:
break
case core.AccountPeerRemoved:
break
}
}
}
}()
// subscribe to thread updates
go func() {
for {
select {
case value, ok := <-listener.Ch:
if !ok {
return
}
if update, ok := value.(core.ThreadUpdate); ok {
date := update.Block.Date.Format(time.RFC822)
desc := update.Block.Type
thrd := update.ThreadId[len(update.ThreadId)-8:]
if update.Block.Username != "" {
update.Block.Username += " "
}
msg := cmd.Grey(date+" "+update.Block.Username+"added ") +
cmd.Green(desc) + cmd.Grey(" update to thread "+thrd)
fmt.Println(msg)
}
}
}
}()
// subscribe to notifications
go func() {
for {
select {
case note, ok := <-node.NotificationCh():
if !ok {
return
}
date := note.Date.Format(time.RFC822)
var subject string
if len(note.SubjectId) >= 7 {
subject = note.SubjectId[len(note.SubjectId)-7:]
}
msg := cmd.Grey(date+" "+note.Username+" ") + cmd.Cyan(note.Body) +
cmd.Grey(" "+subject)
fmt.Println(msg)
}
}
}()
// start apis
node.StartApi(node.Config().Addresses.API)
gateway.Host.Start(node.Config().Addresses.Gateway)
<-node.OnlineCh()
return nil
}
func stopNode() error {
if err := node.StopApi(); err != nil {
return err
}
if err := gateway.Host.Stop(); err != nil {
return err
}
if err := node.Stop(); err != nil {
return err
}
node.CloseChns()
return nil
}
func printSplash() {
pid, err := node.PeerId()
if err != nil {
log.Fatalf("get peer id failed: %s", err)
}
fmt.Println(cmd.Grey("Textile daemon version v" + core.Version))
fmt.Println(cmd.Grey("Repo: ") + cmd.Grey(node.RepoPath()))
fmt.Println(cmd.Grey("API: ") + cmd.Grey(node.ApiAddr()))
fmt.Println(cmd.Grey("Gateway: ") + cmd.Grey(gateway.Host.Addr()))
if node.CafeApiAddr() != "" {
fmt.Println(cmd.Grey("Cafe: ") + cmd.Grey(node.CafeApiAddr()))
}
fmt.Println(cmd.Grey("PeerID: ") + cmd.Green(pid.Pretty()))
fmt.Println(cmd.Grey("Account: ") + cmd.Cyan(node.Account().Address()))
}