Compare commits
192 commits
Author | SHA1 | Date | |
---|---|---|---|
|
a01aa6dc06 | ||
|
e6a3f40029 | ||
|
ced09b22ca | ||
|
fa55e82bc1 | ||
|
77944ba3af | ||
|
5f52a995a7 | ||
|
014adbb315 | ||
|
73228d1bfb | ||
|
69cfd7f798 | ||
|
41555cbda2 | ||
|
2adb8af5b6 | ||
|
9130630afe | ||
|
8e6d493fbf | ||
|
e19facdded | ||
|
365d23f0e2 | ||
|
e5ab0f883e | ||
|
d0aeb0c22b | ||
|
306db74279 | ||
|
a0391bec79 | ||
|
5d62502bde | ||
|
91ac7abf08 | ||
|
8161f48c15 | ||
|
d11230aaf8 | ||
|
8fd87dfc31 | ||
|
4056c44c2e | ||
|
dd451eb72b | ||
|
a553e18d3b | ||
|
3e18b74da0 | ||
|
55dceeaa4e | ||
|
a1177c17d3 | ||
|
2b155597bf | ||
|
87bf89a109 | ||
|
931d786c52 | ||
|
6516df1418 | ||
|
3027fb9b98 | ||
|
ed51ece75c | ||
|
e00cdd0237 | ||
|
6bc878d657 | ||
|
be64130ae1 | ||
|
419e7c88a3 | ||
|
988178df50 | ||
|
a365d63d16 | ||
|
bd452c421f | ||
|
4c3372992c | ||
|
3c99b84721 | ||
|
d7e84c6b97 | ||
|
4580a95b74 | ||
|
29773829af | ||
|
ef1b43ac62 | ||
|
39e5821760 | ||
|
cb68cb004e | ||
|
eb6bb93500 | ||
|
d0df93ebac | ||
|
8c41d8ccd9 | ||
|
e9753ffdc7 | ||
|
69e03da94a | ||
|
b3f7657c1b | ||
|
29574578c1 | ||
|
73382bb021 | ||
|
69e2f6231c | ||
|
fb88808c97 | ||
|
a16797cc53 | ||
|
b14fb6c18b | ||
|
8db975b532 | ||
|
fcade74753 | ||
|
f1d8bc0ffc | ||
|
9d8b9330f9 | ||
|
3ae040d677 | ||
|
07d2d00b0d | ||
|
86d0678274 | ||
|
bdf6240ed8 | ||
|
e8b93e3bb1 | ||
|
9a9be92d27 | ||
|
414be62d61 | ||
|
21017a38a7 | ||
|
1a30fb743b | ||
|
db8aa21b35 | ||
|
c4772e61c5 | ||
|
9541d765a9 | ||
|
f9717328bc | ||
|
1835deb2c9 | ||
|
dc6b15a372 | ||
|
635bf931c8 | ||
|
d12e431b40 | ||
|
8f8b2e605b | ||
|
9b6fea461c | ||
|
a391c83a2f | ||
|
06764c3d00 | ||
|
b68c49ac2f | ||
|
96ace4b850 | ||
|
6782b19c3d | ||
|
bcadcd1eca | ||
|
ef1b10b601 | ||
|
aec378bb36 | ||
|
fa9bab156a | ||
|
de5b32b1b5 | ||
|
dd2171172d | ||
|
1155ea6b9d | ||
|
6918a2436a | ||
|
7e524c6b08 | ||
|
9c278d131d | ||
|
b1c090e28d | ||
|
b03e0a7db8 | ||
|
33071ff6c1 | ||
|
30a9a39248 | ||
|
49c9531404 | ||
|
c68c7e05fe | ||
|
534831e6e5 | ||
|
deab868c53 | ||
|
d5102a9cf6 | ||
|
ac75979453 | ||
|
712e346bd2 | ||
|
85e34cb335 | ||
|
c36c67961f | ||
|
025c715ab4 | ||
|
af728f12d9 | ||
|
1f1848a408 | ||
|
d032c842d5 | ||
|
d6f5199acd | ||
|
28aad86e4a | ||
|
c5c634e477 | ||
|
a8c339e5b4 | ||
|
fd916d9eae | ||
|
2d45f059ec | ||
|
e5850035dd | ||
|
0cfc8e230c | ||
|
f3a1fbdd53 | ||
|
ecbd404da0 | ||
|
2768cdd312 | ||
|
564595cfc3 | ||
|
3a3377d0e5 | ||
|
38861421f8 | ||
|
a7bb3cf336 | ||
|
8fa28d3d65 | ||
|
f8a231286a | ||
|
ea5b70e8fc | ||
|
26b0c7356d | ||
|
7558397877 | ||
|
fb7d045753 | ||
|
4d17553a23 | ||
|
40633c949e | ||
|
a17fa3ad5f | ||
|
560858ba0a | ||
|
ada0ce0484 | ||
|
461ae6a16b | ||
|
31456e7bae | ||
|
060970b7c0 | ||
|
0f894aaecc | ||
|
8367901104 | ||
|
7e445e0cf8 | ||
|
f5de4e96c3 | ||
|
6226c2690d | ||
|
3335233566 | ||
|
faac895509 | ||
|
bc9886b3e8 | ||
|
963177cd4c | ||
|
6d2f69a36f | ||
|
b444d284c6 | ||
|
f853f2a00b | ||
|
a8afd1621e | ||
|
88aad289cf | ||
|
de1476becf | ||
|
d701bab7f6 | ||
|
c54836bca0 | ||
|
322c658307 | ||
|
bad2d869f5 | ||
|
414540ec6d | ||
|
9159c7602d | ||
|
50996a6b0d | ||
|
e29b47a047 | ||
|
38325c6d44 | ||
|
c84b36d76d | ||
|
95c75ed957 | ||
|
ccd1b2b84d | ||
|
dac2dbda61 | ||
|
2e58228e91 | ||
|
185433f2fd | ||
|
40f9c3f961 | ||
|
dd814b834b | ||
|
cdaf3ac682 | ||
|
6d169425d1 | ||
|
f53da5c3e5 | ||
|
d89334f9eb | ||
|
2fcc2f106e | ||
|
4cff2dd233 | ||
|
37a8c4cae1 | ||
|
8aaa786512 | ||
|
1f23a260ae | ||
|
e02762ab6c | ||
|
9a629bb545 | ||
|
c62c175d8b | ||
|
eb8f72f0d8 |
89 changed files with 6258 additions and 793 deletions
|
@ -1,8 +1,8 @@
|
|||
os: linux
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.17.x
|
||||
|
||||
env:
|
||||
global:
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2018 LBRY Inc
|
||||
Copyright (c) 2016-2020 LBRY Inc
|
||||
|
||||
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:
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/lyoshenka/bencode"
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ package bits
|
|||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/lbryio/errors.go"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
// Range has a start and end
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
func TestBootstrapPing(t *testing.T) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -17,7 +17,7 @@ const (
|
|||
|
||||
// TODO: all these constants should be defaults, and should be used to set values in the standard Config. then the code should use values in the config
|
||||
// TODO: alternatively, have a global Config for constants. at least that way tests can modify the values
|
||||
alpha = 3 // this is the constant alpha in the spec
|
||||
alpha = 5 // this is the constant alpha in the spec
|
||||
bucketSize = 8 // this is the constant k in the spec
|
||||
nodeIDLength = bits.NumBytes // bytes. this is the constant B in the spec
|
||||
messageIDLength = 20 // bytes.
|
||||
|
@ -65,9 +65,10 @@ func NewStandardConfig() *Config {
|
|||
return &Config{
|
||||
Address: "0.0.0.0:" + strconv.Itoa(DefaultPort),
|
||||
SeedNodes: []string{
|
||||
"lbrynet1.lbry.io:4444",
|
||||
"lbrynet2.lbry.io:4444",
|
||||
"lbrynet3.lbry.io:4444",
|
||||
"lbrynet1.lbry.com:4444",
|
||||
"lbrynet2.lbry.com:4444",
|
||||
"lbrynet3.lbry.com:4444",
|
||||
"lbrynet4.lbry.com:4444",
|
||||
},
|
||||
PeerProtocolPort: DefaultPeerPort,
|
||||
ReannounceTime: DefaultReannounceTime,
|
||||
|
|
|
@ -2,12 +2,13 @@ package dht
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/lyoshenka/bencode"
|
||||
)
|
||||
|
@ -41,6 +42,20 @@ func (c Contact) String() string {
|
|||
return str
|
||||
}
|
||||
|
||||
func (c Contact) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
ID string
|
||||
IP string
|
||||
Port int
|
||||
PeerPort int
|
||||
}{
|
||||
ID: c.ID.Hex(),
|
||||
IP: c.IP.String(),
|
||||
Port: c.Port,
|
||||
PeerPort: c.PeerPort,
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalCompact returns a compact byteslice representation of the contact
|
||||
// NOTE: The compact representation always uses the tcp PeerPort, not the udp Port. This is dumb, but that's how the python daemon does it
|
||||
func (c Contact) MarshalCompact() ([]byte, error) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
func TestCompactEncoding(t *testing.T) {
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
func TestNodeFinder_FindNodes(t *testing.T) {
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/lyoshenka/bencode"
|
||||
"github.com/spf13/cast"
|
||||
|
@ -40,6 +40,7 @@ const (
|
|||
headerPayloadField = "3"
|
||||
headerArgsField = "4"
|
||||
contactsField = "contacts"
|
||||
pageField = "p"
|
||||
tokenField = "token"
|
||||
protocolVersionField = "protocolVersion"
|
||||
)
|
||||
|
@ -270,6 +271,7 @@ type Response struct {
|
|||
FindValueKey string
|
||||
Token string
|
||||
ProtocolVersion int
|
||||
Page uint8
|
||||
}
|
||||
|
||||
func (r Response) argsDebug() string {
|
||||
|
@ -390,10 +392,18 @@ func (r *Response) UnmarshalBencode(b []byte) error {
|
|||
|
||||
if contacts, ok := rawData[contactsField]; ok {
|
||||
err = bencode.DecodeBytes(contacts, &r.Contacts)
|
||||
delete(rawData, contactsField) // so it doesnt mess up findValue key finding below
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if page, ok := rawData[pageField]; ok {
|
||||
err = bencode.DecodeBytes(page, &r.Page)
|
||||
delete(rawData, pageField) // so it doesnt mess up findValue key finding below
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for k, v := range rawData {
|
||||
r.FindValueKey = k
|
||||
var compactContacts [][]byte
|
||||
|
@ -411,7 +421,6 @@ func (r *Response) UnmarshalBencode(b []byte) error {
|
|||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lyoshenka/bencode"
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/extras/util"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lyoshenka/bencode"
|
||||
|
|
|
@ -4,13 +4,13 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/crypto"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/crypto"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/uber-go/atomic"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
// TODO: iterativeFindValue may be stopping early. if it gets a response with one peer, it should keep going because other nodes may know about more peers that have that blob
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lyoshenka/bencode"
|
||||
)
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
)
|
||||
|
||||
// TODO: if routing table is ever empty (aka the node is isolated), it should re-bootstrap
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
|
||||
"github.com/sebdah/goldie"
|
||||
)
|
||||
|
|
14
dht/rpc.go
14
dht/rpc.go
|
@ -7,8 +7,8 @@ import (
|
|||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
rpc2 "github.com/gorilla/rpc/v2"
|
||||
|
@ -102,6 +102,7 @@ type RpcIterativeFindValueArgs struct {
|
|||
type RpcIterativeFindValueResult struct {
|
||||
Contacts []Contact
|
||||
FoundValue bool
|
||||
Values []Contact
|
||||
}
|
||||
|
||||
func (rpc *rpcReceiver) IterativeFindValue(r *http.Request, args *RpcIterativeFindValueArgs, result *RpcIterativeFindValueResult) error {
|
||||
|
@ -109,12 +110,19 @@ func (rpc *rpcReceiver) IterativeFindValue(r *http.Request, args *RpcIterativeFi
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
foundContacts, found, err := FindContacts(rpc.dht.node, key, false, nil)
|
||||
foundContacts, found, err := FindContacts(rpc.dht.node, key, true, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Contacts = foundContacts
|
||||
result.FoundValue = found
|
||||
if found {
|
||||
for _, contact := range foundContacts {
|
||||
if contact.PeerPort > 0 {
|
||||
result.Values = append(result.Values, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package dht
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
)
|
||||
|
||||
// TODO: expire stored data after tExpire time
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
var testingDHTIP = "127.0.0.1"
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
)
|
||||
|
||||
// TODO: this should be moved out of dht and into node, and it should be completely hidden inside node. dht should not need to know about tokens
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/dht/bits"
|
||||
"github.com/lbryio/lbry.go/extras/stop"
|
||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||
)
|
||||
|
||||
type tokenManager struct {
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/util"
|
||||
"github.com/lbryio/lbry.go/extras/validator"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||
"github.com/lbryio/lbry.go/v2/extras/validator"
|
||||
v "github.com/lbryio/ozzo-validation"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
@ -17,9 +17,28 @@ import (
|
|||
// ResponseHeaders are returned with each response
|
||||
var ResponseHeaders map[string]string
|
||||
|
||||
// CorsDomains Allowed domains for CORS Policy
|
||||
var CorsDomains []string
|
||||
|
||||
// CorsAllowLocalhost if true localhost connections are always allowed
|
||||
var CorsAllowLocalhost bool
|
||||
|
||||
// Log allows logging of events and errors
|
||||
var Log = func(*http.Request, *Response, error) {}
|
||||
|
||||
// http://choly.ca/post/go-json-marshalling/
|
||||
type ResponseInfo struct {
|
||||
Success bool `json:"success"`
|
||||
Error *string `json:"error"`
|
||||
Data interface{} `json:"data"`
|
||||
Trace []string `json:"_trace,omitempty"`
|
||||
}
|
||||
|
||||
// BuildJSONResponse allows implementers to control the json response form from the api
|
||||
var BuildJSONResponse = func(response ResponseInfo) ([]byte, error) {
|
||||
return json.MarshalIndent(&response, "", " ")
|
||||
}
|
||||
|
||||
// TraceEnabled Attaches a trace field to the JSON response when enabled.
|
||||
var TraceEnabled = false
|
||||
|
||||
|
@ -64,6 +83,32 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
origin := r.Header.Get("origin")
|
||||
for _, d := range CorsDomains {
|
||||
if d == origin {
|
||||
w.Header().Set("Access-Control-Allow-Origin", d)
|
||||
vary := w.Header().Get("Vary")
|
||||
if vary != "*" {
|
||||
if vary != "" {
|
||||
vary += ", "
|
||||
}
|
||||
vary += "Origin"
|
||||
}
|
||||
w.Header().Set("Vary", vary)
|
||||
}
|
||||
}
|
||||
|
||||
if CorsAllowLocalhost && strings.HasPrefix(origin, "http://localhost:") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
vary := w.Header().Get("Vary")
|
||||
if vary != "*" {
|
||||
if vary != "" {
|
||||
vary += ", "
|
||||
}
|
||||
vary += "Origin"
|
||||
}
|
||||
w.Header().Set("Vary", vary)
|
||||
}
|
||||
|
||||
// Stop here if its a preflighted OPTIONS request
|
||||
if r.Method == "OPTIONS" {
|
||||
|
@ -76,6 +121,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if rsp.Error != nil {
|
||||
ogErr := errors.Unwrap(rsp.Error)
|
||||
if statusError, ok := ogErr.(StatusError); ok {
|
||||
if statusError.Status == 0 {
|
||||
statusError.Status = http.StatusInternalServerError
|
||||
}
|
||||
rsp.Status = statusError.Status
|
||||
} else {
|
||||
rsp.Status = http.StatusInternalServerError
|
||||
|
@ -113,32 +161,40 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var trace []string
|
||||
if TraceEnabled && errors.HasTrace(rsp.Error) {
|
||||
trace = strings.Split(errors.Trace(rsp.Error), "\n")
|
||||
trace = getTraceFromError(rsp.Error)
|
||||
}
|
||||
|
||||
jsonResponse, err := BuildJSONResponse(ResponseInfo{
|
||||
Success: success,
|
||||
Error: errorString,
|
||||
Data: rsp.Data,
|
||||
Trace: trace,
|
||||
})
|
||||
if err != nil {
|
||||
Log(r, &rsp, errors.Prefix("Error encoding JSON response: ", err))
|
||||
jsonResponse, err = BuildJSONResponse(ResponseInfo{
|
||||
Success: false,
|
||||
Error: util.PtrToString(err.Error()),
|
||||
Data: nil,
|
||||
Trace: getTraceFromError(err),
|
||||
})
|
||||
if err != nil {
|
||||
Log(r, &rsp, errors.Prefix("Error encoding JSON response: ", err))
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(rsp.Status)
|
||||
_, _ = w.Write(jsonResponse)
|
||||
}
|
||||
|
||||
func getTraceFromError(err error) []string {
|
||||
trace := strings.Split(errors.Trace(err), "\n")
|
||||
for index, element := range trace {
|
||||
if strings.HasPrefix(element, "\t") {
|
||||
trace[index] = strings.Replace(element, "\t", " ", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://choly.ca/post/go-json-marshalling/
|
||||
jsonResponse, err := json.MarshalIndent(&struct {
|
||||
Success bool `json:"success"`
|
||||
Error *string `json:"error"`
|
||||
Data interface{} `json:"data"`
|
||||
Trace []string `json:"_trace,omitempty"`
|
||||
}{
|
||||
Success: success,
|
||||
Error: errorString,
|
||||
Data: rsp.Data,
|
||||
Trace: trace,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
Log(r, &rsp, errors.Prefix("Error encoding JSON response: ", err))
|
||||
}
|
||||
|
||||
w.WriteHeader(rsp.Status)
|
||||
w.Write(jsonResponse)
|
||||
return trace
|
||||
}
|
||||
|
||||
// IgnoredFormFields are ignored by FormValues() when checking for extraneous fields
|
||||
|
@ -154,21 +210,25 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
structValue := ref.Elem()
|
||||
fields := map[string]bool{}
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
name := structType.Field(i).Name
|
||||
underscoredName := util.Underscore(name)
|
||||
value := strings.TrimSpace(r.FormValue(underscoredName))
|
||||
fieldName := structType.Field(i).Name
|
||||
formattedName := util.Underscore(fieldName)
|
||||
jsonName, ok := structType.Field(i).Tag.Lookup("json")
|
||||
if ok {
|
||||
formattedName = jsonName
|
||||
}
|
||||
value := strings.TrimSpace(r.FormValue(formattedName))
|
||||
|
||||
// if param is not set at all, continue
|
||||
// comes after call to r.FormValue so form values get parsed internally (if they arent already)
|
||||
if len(r.Form[underscoredName]) == 0 {
|
||||
if len(r.Form[formattedName]) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[underscoredName] = true
|
||||
fields[formattedName] = true
|
||||
isPtr := false
|
||||
var finalValue reflect.Value
|
||||
|
||||
structField := structValue.FieldByName(name)
|
||||
structField := structValue.FieldByName(fieldName)
|
||||
structFieldKind := structField.Kind()
|
||||
if structFieldKind == reflect.Ptr {
|
||||
isPtr = true
|
||||
|
@ -184,7 +244,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
}
|
||||
castVal, err := cast.ToInt64E(value)
|
||||
if err != nil {
|
||||
return errors.Err("%s: must be an integer", underscoredName)
|
||||
return errors.Err("%s: must be an integer", formattedName)
|
||||
}
|
||||
switch structFieldKind {
|
||||
case reflect.Int:
|
||||
|
@ -204,7 +264,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
}
|
||||
castVal, err := cast.ToUint64E(value)
|
||||
if err != nil {
|
||||
return errors.Err("%s: must be an unsigned integer", underscoredName)
|
||||
return errors.Err("%s: must be an unsigned integer", formattedName)
|
||||
}
|
||||
switch structFieldKind {
|
||||
case reflect.Uint:
|
||||
|
@ -224,7 +284,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
}
|
||||
if !validator.IsBoolString(value) {
|
||||
return errors.Err("%s: must be one of the following values: %s",
|
||||
underscoredName, strings.Join(validator.GetBoolStringValues(), ", "))
|
||||
formattedName, strings.Join(validator.GetBoolStringValues(), ", "))
|
||||
}
|
||||
finalValue = reflect.ValueOf(validator.IsTruthy(value))
|
||||
|
||||
|
@ -234,7 +294,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
}
|
||||
castVal, err := cast.ToFloat64E(value)
|
||||
if err != nil {
|
||||
return errors.Err("%s: must be a floating point number", underscoredName)
|
||||
return errors.Err("%s: must be a floating point number", formattedName)
|
||||
}
|
||||
switch structFieldKind {
|
||||
case reflect.Float32:
|
||||
|
@ -243,7 +303,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
|
|||
finalValue = reflect.ValueOf(float64(castVal))
|
||||
}
|
||||
default:
|
||||
return errors.Err("field %s is an unsupported type", name)
|
||||
return errors.Err("field %s is an unsupported type", fieldName)
|
||||
}
|
||||
|
||||
if isPtr {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/btcsuite/btcutil/base58"
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
|
|
@ -10,22 +10,34 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/shopspring/decimal"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/ybbus/jsonrpc"
|
||||
"github.com/ybbus/jsonrpc/v2"
|
||||
)
|
||||
|
||||
const DefaultPort = 5279
|
||||
|
||||
const (
|
||||
ErrorWalletNotLoaded = "WalletNotLoadedError"
|
||||
ErrorWalletAlreadyLoaded = "WalletAlreadyLoadedError"
|
||||
ErrorWalletNotFound = "WalletNotFoundError"
|
||||
ErrorWalletAlreadyExists = "WalletAlreadyExistsError"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn jsonrpc.RPCClient
|
||||
address string
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int
|
||||
Name string
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewClient(address string) *Client {
|
||||
d := Client{}
|
||||
|
||||
|
@ -71,6 +83,15 @@ func Decode(data interface{}, targetStruct interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WrapError adds error metadata from JSONRPC error response for clients to access
|
||||
func WrapError(rpcError *jsonrpc.RPCError) Error {
|
||||
e := Error{Code: rpcError.Code, Message: rpcError.Message}
|
||||
if d, ok := rpcError.Data.(map[string]interface{}); ok {
|
||||
e.Name = d["name"].(string)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func decodeNumber(data interface{}) (decimal.Decimal, error) {
|
||||
var number string
|
||||
|
||||
|
@ -107,6 +128,10 @@ func debugParams(params map[string]interface{}) string {
|
|||
return strings.Join(s, " ")
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("Error in daemon: %s", e.Message)
|
||||
}
|
||||
|
||||
func (d *Client) callNoDecode(command string, params map[string]interface{}) (interface{}, error) {
|
||||
log.Debugln("jsonrpc: " + command + " " + debugParams(params))
|
||||
r, err := d.conn.Call(command, params)
|
||||
|
@ -115,7 +140,7 @@ func (d *Client) callNoDecode(command string, params map[string]interface{}) (in
|
|||
}
|
||||
|
||||
if r.Error != nil {
|
||||
return nil, errors.Err("Error in daemon: " + r.Error.Message)
|
||||
return nil, WrapError(r.Error)
|
||||
}
|
||||
|
||||
return r.Result, nil
|
||||
|
@ -138,28 +163,60 @@ func (d *Client) SetRPCTimeout(timeout time.Duration) {
|
|||
//============================================
|
||||
// NEW SDK
|
||||
//============================================
|
||||
func (d *Client) AccountList() (*AccountListResponse, error) {
|
||||
response := new(AccountListResponse)
|
||||
return response, d.call(response, "account_list", map[string]interface{}{})
|
||||
|
||||
func (d *Client) AccountSend(accountID *string, amount, toAddress string) (*TransactionSummary, error) {
|
||||
response := new(TransactionSummary)
|
||||
args := struct {
|
||||
AccountID *string `json:"account_id"`
|
||||
Amount string `json:"amount"`
|
||||
Addresses string `json:"addresses"`
|
||||
}{
|
||||
AccountID: accountID,
|
||||
Amount: amount,
|
||||
Addresses: toAddress,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "account_send", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) SingleAccountList(accountID string) (*Account, error) {
|
||||
response := new(Account)
|
||||
func (d *Client) AccountList(page uint64, pageSize uint64) (*AccountListResponse, error) {
|
||||
response := new(AccountListResponse)
|
||||
return response, d.call(response, "account_list", map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) AccountListForWallet(walletID string) (*AccountListResponse, error) {
|
||||
response := new(AccountListResponse)
|
||||
return response, d.call(response, "account_list", map[string]interface{}{"wallet_id": walletID})
|
||||
}
|
||||
|
||||
func (d *Client) SingleAccountList(accountID string) (*AccountListResponse, error) {
|
||||
response := new(AccountListResponse)
|
||||
return response, d.call(response, "account_list", map[string]interface{}{"account_id": accountID})
|
||||
}
|
||||
|
||||
type AccountSettings struct {
|
||||
Default bool `json:"default"`
|
||||
NewName string `json:"new_name"`
|
||||
ReceivingGap int `json:"receiving_gap"`
|
||||
ReceivingMaxUses int `json:"receiving_max_uses"`
|
||||
ChangeGap int `json:"change_gap"`
|
||||
ChangeMaxUses int `json:"change_max_uses"`
|
||||
Default *bool `json:"default,omitempty"`
|
||||
NewName *string `json:"new_name,omitempty"`
|
||||
ReceivingGap *int `json:"receiving_gap,omitempty"`
|
||||
ReceivingMaxUses *int `json:"receiving_max_uses,omitempty"`
|
||||
ChangeGap *int `json:"change_gap,omitempty"`
|
||||
ChangeMaxUses *int `json:"change_max_uses,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Client) AccountSet(accountID string, settings AccountSettings) (*Account, error) {
|
||||
response := new(Account)
|
||||
return response, d.call(response, "account_list", map[string]interface{}{})
|
||||
args := struct {
|
||||
AccountID string `json:"account_id"`
|
||||
AccountSettings `json:",flatten"`
|
||||
}{
|
||||
AccountID: accountID,
|
||||
AccountSettings: settings,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "account_set", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) AccountBalance(account *string) (*AccountBalanceResponse, error) {
|
||||
|
@ -169,27 +226,29 @@ func (d *Client) AccountBalance(account *string) (*AccountBalanceResponse, error
|
|||
})
|
||||
}
|
||||
|
||||
func (d *Client) AccountFund(fromAccount string, toAccount string, amount string, outputs uint64) (*AccountFundResponse, error) {
|
||||
// funds an account. If everything is true then amount is ignored
|
||||
func (d *Client) AccountFund(fromAccount string, toAccount string, amount string, outputs uint64, everything bool) (*AccountFundResponse, error) {
|
||||
response := new(AccountFundResponse)
|
||||
return response, d.call(response, "account_fund", map[string]interface{}{
|
||||
"from_account": fromAccount,
|
||||
"to_account": toAccount,
|
||||
"amount": amount,
|
||||
"outputs": outputs,
|
||||
"everything": everything,
|
||||
"broadcast": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) AccountCreate(accountName string, singleKey bool) (*AccountCreateResponse, error) {
|
||||
response := new(AccountCreateResponse)
|
||||
func (d *Client) AccountCreate(accountName string, singleKey bool) (*Account, error) {
|
||||
response := new(Account)
|
||||
return response, d.call(response, "account_create", map[string]interface{}{
|
||||
"account_name": accountName,
|
||||
"single_key": singleKey,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) AccountRemove(accountID string) (*AccountRemoveResponse, error) {
|
||||
response := new(AccountRemoveResponse)
|
||||
func (d *Client) AccountRemove(accountID string) (*Account, error) {
|
||||
response := new(Account)
|
||||
return response, d.call(response, "account_remove", map[string]interface{}{
|
||||
"account_id": accountID,
|
||||
})
|
||||
|
@ -202,7 +261,14 @@ func (d *Client) AddressUnused(account *string) (*AddressUnusedResponse, error)
|
|||
})
|
||||
}
|
||||
|
||||
func (d *Client) ChannelList(account *string, page uint64, pageSize uint64) (*ChannelListResponse, error) {
|
||||
func (d *Client) TransactionShow(txid string) (*TransactionSummary, error) {
|
||||
response := new(TransactionSummary)
|
||||
return response, d.call(response, "transaction_show", map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) ChannelList(account *string, page uint64, pageSize uint64, wid *string) (*ChannelListResponse, error) {
|
||||
if page == 0 {
|
||||
return nil, errors.Err("pages start from 1")
|
||||
}
|
||||
|
@ -212,6 +278,7 @@ func (d *Client) ChannelList(account *string, page uint64, pageSize uint64) (*Ch
|
|||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"include_protobuf": true,
|
||||
"wallet_id": wid,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -232,8 +299,8 @@ type Location struct {
|
|||
Longitude *string `json:"longitude,omitempty"`
|
||||
}
|
||||
type ClaimCreateOptions struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Languages []string `json:"languages,omitempty"`
|
||||
Locations []Location `json:"locations,omitempty"`
|
||||
|
@ -241,6 +308,7 @@ type ClaimCreateOptions struct {
|
|||
AccountID *string `json:"account_id,omitempty"`
|
||||
ClaimAddress *string `json:"claim_address,omitempty"`
|
||||
Preview *bool `json:"preview,omitempty"`
|
||||
FundingAccountIDs []string `json:"funding_account_ids,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelCreateOptions struct {
|
||||
|
@ -249,6 +317,8 @@ type ChannelCreateOptions struct {
|
|||
WebsiteURL *string `json:"website_url,omitempty"`
|
||||
CoverURL *string `json:"cover_url,omitempty"`
|
||||
Featured []string `json:"featured,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
FundingAccountIDs []string `json:"funding_account_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Client) ChannelCreate(name string, bid float64, options ChannelCreateOptions) (*TransactionSummary, error) {
|
||||
|
@ -259,11 +329,13 @@ func (d *Client) ChannelCreate(name string, bid float64, options ChannelCreateOp
|
|||
FilePath string `json:"file_path,omitempty"`
|
||||
IncludeProtoBuf bool `json:"include_protobuf"`
|
||||
ChannelCreateOptions `json:",flatten"`
|
||||
Blocking bool `json:"blocking"`
|
||||
}{
|
||||
Name: name,
|
||||
Bid: fmt.Sprintf("%.6f", bid),
|
||||
IncludeProtoBuf: true,
|
||||
ChannelCreateOptions: options,
|
||||
Blocking: true,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "channel_create", structs.Map(args))
|
||||
|
@ -276,6 +348,7 @@ type ChannelUpdateOptions struct {
|
|||
ClearTags *bool `json:"clear_tags,omitempty"`
|
||||
ClearLanguages *bool `json:"clear_languages,omitempty"`
|
||||
ClearLocations *bool `json:"clear_locations,omitempty"`
|
||||
Bid *string `json:"bid,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Client) ChannelUpdate(claimID string, options ChannelUpdateOptions) (*TransactionSummary, error) {
|
||||
|
@ -284,10 +357,12 @@ func (d *Client) ChannelUpdate(claimID string, options ChannelUpdateOptions) (*T
|
|||
ClaimID string `json:"claim_id"`
|
||||
IncludeProtoBuf bool `json:"include_protobuf"`
|
||||
*ChannelUpdateOptions `json:",flatten"`
|
||||
Blocking bool `json:"blocking"`
|
||||
}{
|
||||
ClaimID: claimID,
|
||||
IncludeProtoBuf: true,
|
||||
ChannelUpdateOptions: &options,
|
||||
Blocking: true,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "channel_update", structs.Map(args))
|
||||
|
@ -302,15 +377,14 @@ type StreamCreateOptions struct {
|
|||
StreamType *streamType `json:"stream_type,omitempty"`
|
||||
ReleaseTime *int64 `json:"release_time,omitempty"`
|
||||
Duration *uint64 `json:"duration,omitempty"`
|
||||
ImageWidth *uint `json:"image_width,omitempty"`
|
||||
ImageHeight *uint `json:"image_height,omitempty"`
|
||||
VideoWidth *uint `json:"video_width,omitempty"`
|
||||
VideoHeight *uint `json:"video_height,omitempty"`
|
||||
Width *uint `json:"width,omitempty"`
|
||||
Height *uint `json:"height,omitempty"`
|
||||
Preview *string `json:"preview,omitempty"`
|
||||
AllowDuplicateName *bool `json:"allow_duplicate_name,omitempty"`
|
||||
ChannelName *string `json:"channel_name,omitempty"`
|
||||
ChannelID *string `json:"channel_id,omitempty"`
|
||||
ChannelAccountID *string `json:"channel_account_id,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Client) StreamCreate(name, filePath string, bid float64, options StreamCreateOptions) (*TransactionSummary, error) {
|
||||
|
@ -321,12 +395,14 @@ func (d *Client) StreamCreate(name, filePath string, bid float64, options Stream
|
|||
FilePath string `json:"file_path,omitempty"`
|
||||
FileSize *string `json:"file_size,omitempty"`
|
||||
IncludeProtoBuf bool `json:"include_protobuf"`
|
||||
Blocking bool `json:"blocking"`
|
||||
*StreamCreateOptions `json:",flatten"`
|
||||
}{
|
||||
Name: name,
|
||||
FilePath: filePath,
|
||||
Bid: fmt.Sprintf("%.6f", bid),
|
||||
IncludeProtoBuf: true,
|
||||
Blocking: true,
|
||||
StreamCreateOptions: &options,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
|
@ -335,11 +411,12 @@ func (d *Client) StreamCreate(name, filePath string, bid float64, options Stream
|
|||
|
||||
func (d *Client) StreamAbandon(txID string, nOut uint64, accountID *string, blocking bool) (*ClaimAbandonResponse, error) {
|
||||
response := new(ClaimAbandonResponse)
|
||||
err := d.call(response, "claim_abandon", map[string]interface{}{
|
||||
err := d.call(response, "stream_abandon", map[string]interface{}{
|
||||
"txid": txID,
|
||||
"nout": nOut,
|
||||
"account_id": accountID,
|
||||
"include_protobuf": true,
|
||||
"blocking": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -365,22 +442,25 @@ func (d *Client) StreamUpdate(claimID string, options StreamUpdateOptions) (*Tra
|
|||
ClaimID string `json:"claim_id"`
|
||||
IncludeProtoBuf bool `json:"include_protobuf"`
|
||||
*StreamUpdateOptions `json:",flatten"`
|
||||
Blocking bool `json:"blocking"`
|
||||
}{
|
||||
ClaimID: claimID,
|
||||
IncludeProtoBuf: true,
|
||||
StreamUpdateOptions: &options,
|
||||
Blocking: true,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "stream_update", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) ChannelAbandon(txID string, nOut uint64, accountID *string, blocking bool) (*ClaimAbandonResponse, error) {
|
||||
response := new(ClaimAbandonResponse)
|
||||
err := d.call(response, "claim_abandon", map[string]interface{}{
|
||||
func (d *Client) ChannelAbandon(txID string, nOut uint64, accountID *string, blocking bool) (*TransactionSummary, error) {
|
||||
response := new(TransactionSummary)
|
||||
err := d.call(response, "channel_abandon", map[string]interface{}{
|
||||
"txid": txID,
|
||||
"nout": nOut,
|
||||
"account_id": accountID,
|
||||
"include_protobuf": true,
|
||||
"blocking": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -388,11 +468,36 @@ func (d *Client) ChannelAbandon(txID string, nOut uint64, accountID *string, blo
|
|||
return response, nil
|
||||
}
|
||||
|
||||
func (d *Client) AddressList(account *string) (*AddressListResponse, error) {
|
||||
func (d *Client) AddressList(account *string, address *string, page uint64, pageSize uint64) (*AddressListResponse, error) {
|
||||
response := new(AddressListResponse)
|
||||
return response, d.call(response, "address_list", map[string]interface{}{
|
||||
|
||||
args := struct {
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
}{
|
||||
AccountID: account,
|
||||
Address: address,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "address_list", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) StreamList(account *string, page uint64, pageSize uint64) (*StreamListResponse, error) {
|
||||
response := new(StreamListResponse)
|
||||
err := d.call(response, "stream_list", map[string]interface{}{
|
||||
"account_id": account,
|
||||
"include_protobuf": true,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (d *Client) ClaimList(account *string, page uint64, pageSize uint64) (*ClaimListResponse, error) {
|
||||
|
@ -417,10 +522,29 @@ func (d *Client) Status() (*StatusResponse, error) {
|
|||
return response, d.call(response, "status", map[string]interface{}{})
|
||||
}
|
||||
|
||||
func (d *Client) UTXOList(account *string) (*UTXOListResponse, error) {
|
||||
func (d *Client) TransactionList(account *string, wallet *string, page uint64, pageSize uint64) (*TransactionListResponse, error) {
|
||||
response := new(TransactionListResponse)
|
||||
return response, d.call(response, "transaction_list", map[string]interface{}{
|
||||
"account_id": account,
|
||||
"wallet_id": wallet,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) UTXOList(account *string, page uint64, pageSize uint64) (*UTXOListResponse, error) {
|
||||
response := new(UTXOListResponse)
|
||||
return response, d.call(response, "utxo_list", map[string]interface{}{
|
||||
"account_id": account,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) UTXORelease(account *string) (*UTXOReleaseResponse, error) {
|
||||
response := new(UTXOReleaseResponse)
|
||||
return response, d.call(response, "utxo_release", map[string]interface{}{
|
||||
"account_id": account,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -432,10 +556,12 @@ func (d *Client) Get(uri string) (*GetResponse, error) {
|
|||
})
|
||||
}
|
||||
|
||||
func (d *Client) FileList() (*FileListResponse, error) {
|
||||
func (d *Client) FileList(page uint64, pageSize uint64) (*FileListResponse, error) {
|
||||
response := new(FileListResponse)
|
||||
return response, d.call(response, "file_list", map[string]interface{}{
|
||||
"include_protobuf": true,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -452,38 +578,202 @@ func (d *Client) Resolve(urls string) (*ResolveResponse, error) {
|
|||
})
|
||||
}
|
||||
|
||||
/*
|
||||
// use resolve?
|
||||
func (d *Client) NumClaimsInChannel(channelClaimID string) (uint64, error) {
|
||||
response := new(NumClaimsInChannelResponse)
|
||||
err := d.call(response, "claim_search", map[string]interface{}{
|
||||
"channel_id": channelClaimID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if response == nil {
|
||||
return 0, errors.Err("no response")
|
||||
type ClaimSearchArgs struct {
|
||||
ClaimID *string `json:"claim_id,omitempty"`
|
||||
TXID *string `json:"txid,omitempty"`
|
||||
Nout *uint `json:"nout,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ClaimType []string `json:"claim_type,omitempty"`
|
||||
OrderBy []string `json:"order_by,omitempty"`
|
||||
LimitClaimsPerChannel *int `json:"limit_claims_per_channel,omitempty"`
|
||||
HasNoSource *bool `json:"has_no_source,omitempty"`
|
||||
ReleaseTime string `json:"release_time,omitempty"`
|
||||
ChannelIDs []string `json:"channel_ids,omitempty"`
|
||||
NoTotals *bool `json:"no_totals,omitempty"`
|
||||
IncludeProtobuf *bool `json:"include_protobuf,omitempty"`
|
||||
AnyTags []string `json:"any_tags,omitempty"`
|
||||
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
}
|
||||
|
||||
func (d *Client) ClaimSearch(args ClaimSearchArgs) (*ClaimSearchResponse, error) {
|
||||
response := new(ClaimSearchResponse)
|
||||
if args.NoTotals == nil {
|
||||
nototals := true
|
||||
args.NoTotals = ¬otals
|
||||
}
|
||||
|
||||
channel, ok := (*response)[uri]
|
||||
if !ok {
|
||||
return 0, errors.Err("url not in response")
|
||||
if args.IncludeProtobuf == nil {
|
||||
include := true
|
||||
args.IncludeProtobuf = &include
|
||||
}
|
||||
if channel.Error != nil {
|
||||
if strings.Contains(*channel.Error, "cannot be resolved") {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, errors.Err(*channel.Error)
|
||||
}
|
||||
return *channel.ClaimsInChannel, nil
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "claim_search", structs.Map(args))
|
||||
}
|
||||
*/
|
||||
func (d *Client) ClaimSearch(claimName, claimID, txid *string, nout *uint) (*ClaimSearchResponse, error) {
|
||||
response := new(ClaimSearchResponse)
|
||||
return response, d.call(response, "claim_search", map[string]interface{}{
|
||||
"claim_id": claimID,
|
||||
"txid": txid,
|
||||
"nout": nout,
|
||||
"name": claimName,
|
||||
|
||||
func (d *Client) ChannelExport(channelClaimID string, channelName, accountID *string) (*ChannelExportResponse, error) {
|
||||
response := new(ChannelExportResponse)
|
||||
return response, d.call(response, "channel_export", map[string]interface{}{
|
||||
"channel_id": channelClaimID,
|
||||
"channel_name": channelName,
|
||||
"account_id": accountID,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) ChannelImport(key string, walletID *string) (*ChannelImportResponse, error) {
|
||||
response := new(ChannelImportResponse)
|
||||
return response, d.call(response, "channel_import", map[string]interface{}{
|
||||
"channel_data": key,
|
||||
"wallet_id": walletID,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) SupportList(accountID *string, page uint64, pageSize uint64) (*SupportListResponse, error) {
|
||||
response := new(SupportListResponse)
|
||||
return response, d.call(response, "support_list", map[string]interface{}{
|
||||
"account_id": accountID,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Client) SupportCreate(claimID string, amount string, tip *bool, accountID *string, fundingAccountIDs []string, walletID *string) (*TransactionSummary, error) {
|
||||
response := new(TransactionSummary)
|
||||
args := struct {
|
||||
ClaimID string `json:"claim_id"`
|
||||
Amount string `json:"amount"`
|
||||
Tip *bool `json:"tip,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
FundingAccountIDs []string `json:"funding_account_ids,omitempty"`
|
||||
Preview bool `json:"preview,omitempty"`
|
||||
Blocking bool `json:"blocking,omitempty"`
|
||||
WalletID *string `json:"wallet_id,omitempty"`
|
||||
}{
|
||||
ClaimID: claimID,
|
||||
AccountID: accountID,
|
||||
Blocking: true,
|
||||
Amount: amount,
|
||||
FundingAccountIDs: fundingAccountIDs,
|
||||
Preview: false,
|
||||
Tip: tip,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "support_create", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) SupportAbandon(claimID *string, txid *string, nout *uint, keep *string, accountID *string) (*TransactionSummary, error) {
|
||||
if claimID == nil && (txid == nil || nout == nil) {
|
||||
return nil, errors.Err("either claimID or txid+nout must be supplied")
|
||||
}
|
||||
response := new(TransactionSummary)
|
||||
args := struct {
|
||||
ClaimID *string `json:"claim_id,omitempty"`
|
||||
TxID *string `json:"claim_id,omitempty"`
|
||||
Nout *uint `json:"nout,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
Preview bool `json:"preview,omitempty"`
|
||||
Blocking bool `json:"blocking,omitempty"`
|
||||
}{
|
||||
ClaimID: claimID,
|
||||
AccountID: accountID,
|
||||
Nout: nout,
|
||||
TxID: txid,
|
||||
Blocking: true,
|
||||
Preview: false,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "support_abandon", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) TxoSpend(txoType, claimID, txid, channelID, name, accountID *string) (*[]TransactionSummary, error) {
|
||||
if txoType == nil && claimID == nil && txid == nil && channelID == nil && name == nil {
|
||||
return nil, errors.Err("either txoType or claimID or channelID or name or txid must be supplied")
|
||||
}
|
||||
response := new([]TransactionSummary)
|
||||
args := struct {
|
||||
ClaimID *string `json:"claim_id,omitempty"`
|
||||
ChannelID *string `json:"channel_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
TxID *string `json:"claim_id,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
Preview bool `json:"preview,omitempty"`
|
||||
Blocking bool `json:"blocking,omitempty"`
|
||||
IncludeFullTx bool `json:"include_full_tx,omitempty"`
|
||||
}{
|
||||
ClaimID: claimID,
|
||||
ChannelID: channelID,
|
||||
Name: name,
|
||||
Type: txoType,
|
||||
AccountID: accountID,
|
||||
TxID: txid,
|
||||
Blocking: true,
|
||||
Preview: false,
|
||||
IncludeFullTx: true,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "txo_spend", structs.Map(args))
|
||||
}
|
||||
|
||||
func (d *Client) AccountAdd(accountName string, seed *string, privateKey *string, publicKey *string, singleKey *bool, walletID *string) (*Account, error) {
|
||||
response := new(Account)
|
||||
|
||||
args := struct {
|
||||
AccountName string `json:"account_name"`
|
||||
Seed *string `json:"seed,omitempty"`
|
||||
PrivateKey *string `json:"private_key,omitempty"`
|
||||
PublicKey *string `json:"public_key,omitempty"`
|
||||
SingleKey *bool `json:"single_key,omitempty"`
|
||||
WalletID *string `json:"wallet_id,omitempty"`
|
||||
}{
|
||||
AccountName: accountName,
|
||||
Seed: seed,
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
SingleKey: singleKey,
|
||||
WalletID: walletID,
|
||||
}
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "account_add", structs.Map(args))
|
||||
}
|
||||
|
||||
type WalletCreateOpts struct {
|
||||
ID string `json:"wallet_id"`
|
||||
SkipOnStartup bool `json:"skip_on_startup,omitempty"`
|
||||
CreateAccount bool `json:"create_account,omitempty"`
|
||||
SingleKey bool `json:"single_key,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Client) WalletCreate(id string, opts *WalletCreateOpts) (*Wallet, error) {
|
||||
response := new(Wallet)
|
||||
if opts == nil {
|
||||
opts = &WalletCreateOpts{}
|
||||
}
|
||||
opts.ID = id
|
||||
structs.DefaultTagName = "json"
|
||||
return response, d.call(response, "wallet_create", structs.Map(opts))
|
||||
}
|
||||
|
||||
func (d *Client) WalletAdd(id string) (*Wallet, error) {
|
||||
response := new(Wallet)
|
||||
return response, d.call(response, "wallet_add", map[string]interface{}{"wallet_id": id})
|
||||
}
|
||||
|
||||
func (d *Client) WalletList(id string, page uint64, pageSize uint64) (*WalletList, error) {
|
||||
response := new(WalletList)
|
||||
params := map[string]interface {
|
||||
}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
if id != "" {
|
||||
params["wallet_id"] = id
|
||||
}
|
||||
return response, d.call(response, "wallet_list", params)
|
||||
}
|
||||
|
||||
func (d *Client) WalletRemove(id string) (*Wallet, error) {
|
||||
response := new(Wallet)
|
||||
return response, d.call(response, "wallet_remove", map[string]interface{}{"wallet_id": id})
|
||||
}
|
||||
|
|
|
@ -3,14 +3,19 @@ package jsonrpc
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/util"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||
)
|
||||
|
||||
func prettyPrint(i interface{}) {
|
||||
|
@ -18,48 +23,93 @@ func prettyPrint(i interface{}) {
|
|||
fmt.Println(string(s))
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestClient_AccountFund(t *testing.T) {
|
||||
d := NewClient("")
|
||||
accounts, err := d.AccountList()
|
||||
accounts, err := d.AccountList(1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
account := (accounts.LBCRegtest)[0].ID
|
||||
account := (accounts.Items)[0].ID
|
||||
balanceString, err := d.AccountBalance(&account)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
balance, err := strconv.ParseFloat(string(*balanceString), 64)
|
||||
balance, err := strconv.ParseFloat(balanceString.Available.String(), 64)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
got, err := d.AccountFund(account, account, fmt.Sprintf("%f", balance/2.0), 40)
|
||||
got, err := d.AccountFund(account, account, fmt.Sprintf("%f", balance/2.0), 40, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_AccountSend(t *testing.T) {
|
||||
d := NewClient("")
|
||||
accounts, err := d.AccountList(1, 20)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
if !assert.NotEmpty(t, accounts.Items[1].ID) {
|
||||
return
|
||||
}
|
||||
account := (accounts.Items)[1].ID
|
||||
|
||||
addressess, err := d.AddressList(&account, nil, 1, 20)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
if !assert.NotEmpty(t, addressess.Items) {
|
||||
return
|
||||
}
|
||||
|
||||
got, err := d.AccountSend(&account, "0.01", string(addressess.Items[0].Address))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_AccountList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.AccountList()
|
||||
got, err := d.AccountList(1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_SingleAccountList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
createdAccount, err := d.AccountCreate("test"+fmt.Sprintf("%d", time.Now().Unix())+"@lbry.com", false)
|
||||
name := "test" + fmt.Sprintf("%d", rand.Int()) + "@lbry.com"
|
||||
createdAccount, err := d.AccountCreate(name, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account, err := d.SingleAccountList(createdAccount.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
prettyPrint(*createdAccount)
|
||||
prettyPrint(*account)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prettyPrint(*account)
|
||||
if account.Items[0].Name != name {
|
||||
t.Fatalf("account name mismatch: %v != %v", account.Items[0].Name, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AccountBalance(t *testing.T) {
|
||||
|
@ -67,6 +117,7 @@ func TestClient_AccountBalance(t *testing.T) {
|
|||
got, err := d.AccountBalance(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
@ -76,30 +127,70 @@ func TestClient_AddressUnused(t *testing.T) {
|
|||
got, err := d.AddressUnused(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_ChannelList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelList(nil, 1, 50)
|
||||
got, err := d.ChannelList(nil, 1, 50, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
var channelID string
|
||||
|
||||
func TestClient_ChannelCreate(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelCreate("@Test"+fmt.Sprintf("%d", time.Now().Unix()), 1.337, ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: util.PtrToString("Mess with the channels"),
|
||||
Description: util.PtrToString("And you'll get what you deserve"),
|
||||
Tags: []string{"we", "got", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
Country: util.PtrToString("CH"),
|
||||
State: util.PtrToString("Ticino"),
|
||||
City: util.PtrToString("Lugano"),
|
||||
}},
|
||||
ThumbnailURL: util.PtrToString("https://scrn.storni.info/2022-06-10_17-18-29-409175881.png"),
|
||||
},
|
||||
Email: util.PtrToString("niko@lbry.com"),
|
||||
WebsiteURL: util.PtrToString("https://lbry.com"),
|
||||
CoverURL: util.PtrToString("https://scrn.storni.info/2022-06-10_17-18-29-409175881.png"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
channelID = got.Outputs[0].ClaimID
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_StreamCreate(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
addressResponse, err := d.AddressUnused(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
address := string(*addressResponse)
|
||||
got, err := d.StreamCreate("test"+fmt.Sprintf("%d", time.Now().Unix()), "/home/niko/work2/2019-04-11_17-36-25-925698088.png", 14.37, StreamCreateOptions{
|
||||
f, e := os.OpenFile("/tmp/test.txt", os.O_RDONLY|os.O_CREATE, 0666)
|
||||
if e != nil {
|
||||
t.Error(e)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = f.WriteString("test")
|
||||
got, err := d.StreamCreate("test"+fmt.Sprintf("%d", time.Now().Unix()), "/tmp/test.txt", 1.437, StreamCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: "This is a Test Title" + fmt.Sprintf("%d", time.Now().Unix()),
|
||||
Description: "My Special Description",
|
||||
Title: util.PtrToString("This is a Test Title" + fmt.Sprintf("%d", time.Now().Unix())),
|
||||
Description: util.PtrToString("My Special Description"),
|
||||
Tags: []string{"nsfw", "test"},
|
||||
Languages: []string{"en-US", "fr-CH"},
|
||||
Locations: []Location{{
|
||||
|
@ -129,50 +220,27 @@ func TestClient_StreamCreate(t *testing.T) {
|
|||
Preview: nil,
|
||||
AllowDuplicateName: nil,
|
||||
ChannelName: nil,
|
||||
ChannelID: util.PtrToString("2e28aa6dbd41f959893907841f4e40d0ecb0ede9"),
|
||||
ChannelID: util.PtrToString(channelID),
|
||||
ChannelAccountID: nil,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_ChannelCreate(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelCreate("@Test"+fmt.Sprintf("%d", time.Now().Unix()), 13.37, ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: "Mess with the channels",
|
||||
Description: "And you'll get what you deserve",
|
||||
Tags: []string{"we", "got", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
Country: util.PtrToString("CH"),
|
||||
State: util.PtrToString("Ticino"),
|
||||
City: util.PtrToString("Lugano"),
|
||||
}},
|
||||
ThumbnailURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
},
|
||||
Email: util.PtrToString("niko@lbry.com"),
|
||||
WebsiteURL: util.PtrToString("https://lbry.com"),
|
||||
CoverURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_ChannelUpdate(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelUpdate("709868122fe3560a3929d6d63bdbc792d8306a6c", ChannelUpdateOptions{
|
||||
got, err := d.ChannelUpdate(channelID, ChannelUpdateOptions{
|
||||
Bid: util.PtrToString("0.01"),
|
||||
ClearLanguages: util.PtrToBool(true),
|
||||
ClearLocations: util.PtrToBool(true),
|
||||
ClearTags: util.PtrToBool(true),
|
||||
ChannelCreateOptions: ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: "Mess with the channels",
|
||||
Description: "And you'll get what you deserve",
|
||||
Title: util.PtrToString("Mess with the channels"),
|
||||
Description: util.PtrToString("And you'll get what you deserve"),
|
||||
Tags: []string{"we", "got", "more", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
|
@ -188,6 +256,7 @@ func TestClient_ChannelUpdate(t *testing.T) {
|
|||
}})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
@ -197,8 +266,8 @@ func TestClient_ChannelAbandon(t *testing.T) {
|
|||
channelName := "@TestToDelete" + fmt.Sprintf("%d", time.Now().Unix())
|
||||
channelResponse, err := d.ChannelCreate(channelName, 13.37, ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: "Mess with the channels",
|
||||
Description: "And you'll get what you deserve",
|
||||
Title: util.PtrToString("Mess with the channels"),
|
||||
Description: util.PtrToString("And you'll get what you deserve"),
|
||||
Tags: []string{"we", "got", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
|
@ -214,6 +283,7 @@ func TestClient_ChannelAbandon(t *testing.T) {
|
|||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
txID := channelResponse.Outputs[0].Txid
|
||||
nout := channelResponse.Outputs[0].Nout
|
||||
|
@ -221,33 +291,216 @@ func TestClient_ChannelAbandon(t *testing.T) {
|
|||
got, err := d.ChannelAbandon(txID, nout, nil, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_AddressList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.AddressList(nil)
|
||||
got, err := d.AddressList(nil, nil, 1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_ClaimList(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.ClaimList(nil, 1, 10)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_ClaimSearch(t *testing.T) {
|
||||
func TestClient_StreamList(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.ClaimSearch(nil, util.PtrToString("1b2b530dfcef9885354f8f41190c8f678da5414e"), nil, nil)
|
||||
got, err := d.StreamList(nil, 1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_TransactionList(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.TransactionList(nil, nil, 1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_SupportTest(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelCreate("@Test"+fmt.Sprintf("%d", time.Now().Unix()), 13.37, ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: util.PtrToString("Mess with the channels"),
|
||||
Description: util.PtrToString("And you'll get what you deserve"),
|
||||
Tags: []string{"we", "got", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
Country: util.PtrToString("CH"),
|
||||
State: util.PtrToString("Ticino"),
|
||||
City: util.PtrToString("Lugano"),
|
||||
}},
|
||||
ThumbnailURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
},
|
||||
Email: util.PtrToString("niko@lbry.com"),
|
||||
WebsiteURL: util.PtrToString("https://lbry.com"),
|
||||
CoverURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
got2, err := d.SupportCreate(got.Outputs[0].ClaimID, "1.0", util.PtrToBool(true), nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got2)
|
||||
|
||||
got3, err := d.SupportList(nil, 1, 10)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, support := range got3.Items {
|
||||
if support.ClaimID == got.Outputs[0].ClaimID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error(errors.Err("support not found"))
|
||||
return
|
||||
}
|
||||
prettyPrint(*got3)
|
||||
got4, err := d.SupportAbandon(util.PtrToString(got.Outputs[0].ClaimID), nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got4)
|
||||
}
|
||||
|
||||
func TestClient_TxoSpendTest(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.ChannelCreate("@Test"+fmt.Sprintf("%d", time.Now().Unix()), 13.37, ChannelCreateOptions{
|
||||
ClaimCreateOptions: ClaimCreateOptions{
|
||||
Title: util.PtrToString("Mess with the channels"),
|
||||
Description: util.PtrToString("And you'll get what you deserve"),
|
||||
Tags: []string{"we", "got", "tags"},
|
||||
Languages: []string{"en-US"},
|
||||
Locations: []Location{{
|
||||
Country: util.PtrToString("CH"),
|
||||
State: util.PtrToString("Ticino"),
|
||||
City: util.PtrToString("Lugano"),
|
||||
}},
|
||||
ThumbnailURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
},
|
||||
Email: util.PtrToString("niko@lbry.com"),
|
||||
WebsiteURL: util.PtrToString("https://lbry.com"),
|
||||
CoverURL: util.PtrToString("https://scrn.storni.info/2019-04-12_15-43-25-001592625.png"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
got2, err := d.SupportCreate(got.Outputs[0].ClaimID, "1.0", util.PtrToBool(true), nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got2)
|
||||
|
||||
got3, err := d.SupportList(nil, 1, 10)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, support := range got3.Items {
|
||||
if support.ClaimID == got.Outputs[0].ClaimID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error(errors.Err("support not found"))
|
||||
return
|
||||
}
|
||||
prettyPrint(*got3)
|
||||
got4, err := d.TxoSpend(util.PtrToString("support"), util.PtrToString(got.Outputs[0].ClaimID), nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got4)
|
||||
time.Sleep(10 * time.Second)
|
||||
got3, err = d.SupportList(nil, 1, 10)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
found = false
|
||||
for _, support := range got3.Items {
|
||||
if support.ClaimID == got.Outputs[0].ClaimID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.Error(errors.Err("support found even though it should have been abandoned"))
|
||||
return
|
||||
}
|
||||
prettyPrint(*got3)
|
||||
got4, err = d.TxoSpend(util.PtrToString("channel"), util.PtrToString(got.Outputs[0].ClaimID), nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got4)
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
got5, err := d.ClaimList(nil, 1, 50)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for _, claim := range got5.Claims {
|
||||
if claim.ClaimID == got.Outputs[0].ClaimID {
|
||||
t.Error(errors.Err("claim found even though it should have been abandoned"))
|
||||
return
|
||||
}
|
||||
}
|
||||
prettyPrint(*got5)
|
||||
}
|
||||
|
||||
func TestClient_ClaimSearch(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.ClaimSearch(ClaimSearchArgs{
|
||||
ChannelIDs: []string{channelID},
|
||||
ReleaseTime: ">1633350820",
|
||||
HasNoSource: util.PtrToBool(true),
|
||||
OrderBy: []string{"^release_time"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
@ -257,15 +510,17 @@ func TestClient_Status(t *testing.T) {
|
|||
got, err := d.Status()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_UTXOList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
got, err := d.UTXOList(nil)
|
||||
got, err := d.UTXOList(nil, 1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
@ -275,86 +530,316 @@ func TestClient_Version(t *testing.T) {
|
|||
got, err := d.Version()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_GetFile(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.Get("lbry://test1555965264")
|
||||
got, err := d.Get("lbry://test1559058649")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_FileList(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.FileList()
|
||||
got, err := d.FileList(1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_Resolve(t *testing.T) {
|
||||
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
|
||||
d := NewClient("")
|
||||
got, err := d.Resolve("test1555965264")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
got, err := d.Resolve("test1559058649")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_AccountSet(t *testing.T) {
|
||||
d := NewClient("")
|
||||
accounts, err := d.AccountList()
|
||||
accounts, err := d.AccountList(1, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
account := (accounts.LBCRegtest)[0].ID
|
||||
account := (accounts.Items)[0].ID
|
||||
|
||||
got, err := d.AccountSet(account, AccountSettings{ChangeMaxUses: 10000})
|
||||
got, err := d.AccountSet(account, AccountSettings{ChangeMaxUses: util.PtrToInt(10000)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
prettyPrint(*got)
|
||||
}
|
||||
|
||||
func TestClient_AccountCreate(t *testing.T) {
|
||||
d := NewClient("")
|
||||
name := "test" + fmt.Sprintf("%d", time.Now().Unix()) + "@lbry.com"
|
||||
name := "lbry#user#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
account, err := d.AccountCreate(name, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
if account.Name != name {
|
||||
t.Errorf("account name mismatch, expected %q, got %q", name, account.Name)
|
||||
return
|
||||
}
|
||||
prettyPrint(*account)
|
||||
}
|
||||
|
||||
func TestClient_AccountAdd(t *testing.T) {
|
||||
d := NewClient("")
|
||||
name := "test" + fmt.Sprintf("%d", time.Now().Unix()) + "@lbry.com"
|
||||
pubKey := "tpubDA9GDAntyJu4hD3wU7175p7CuV6DWbYXfyb2HedBA3yuBp9HZ4n3QE4Ex6RHCSiEuVp2nKAL1Lzf2ZLo9ApaFgNaJjG6Xo1wB3iEeVbrDZp"
|
||||
account, err := d.AccountAdd(name, nil, nil, &pubKey, util.PtrToBool(true), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
if account.Name != name {
|
||||
t.Errorf("account name mismatch, expected %q, got %q", name, account.Name)
|
||||
return
|
||||
}
|
||||
if account.PublicKey != pubKey {
|
||||
t.Errorf("public key mismatch, expected %q, got %q", name, account.Name)
|
||||
return
|
||||
}
|
||||
prettyPrint(*account)
|
||||
}
|
||||
|
||||
func TestClient_AccountRemove(t *testing.T) {
|
||||
d := NewClient("")
|
||||
createdAccount, err := d.AccountCreate("test"+fmt.Sprintf("%d", time.Now().Unix())+"@lbry.com", false)
|
||||
name := "lbry#user#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
createdAccount, err := d.AccountCreate(name, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
removedAccount, err := d.AccountRemove(createdAccount.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if removedAccount.ID != createdAccount.ID {
|
||||
t.Error("accounts IDs mismatch")
|
||||
}
|
||||
|
||||
account, err := d.SingleAccountList(createdAccount.ID)
|
||||
if !strings.HasPrefix(err.Error(), "Error in daemon: Couldn't find account") {
|
||||
t.Error("account was not removed")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Couldn't find account:") {
|
||||
prettyPrint(*removedAccount)
|
||||
return
|
||||
}
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Error("account was not removed")
|
||||
prettyPrint(*account)
|
||||
}
|
||||
|
||||
func TestClient_ChannelExport(t *testing.T) {
|
||||
d := NewClient("")
|
||||
response, err := d.ChannelExport(channelID, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if response == nil || len(*response) == 0 {
|
||||
t.Error("nothing returned!")
|
||||
}
|
||||
t.Log("Export:", *response)
|
||||
}
|
||||
|
||||
func TestClient_ChannelImport(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
// A channel created just for automated testing purposes
|
||||
channelName := "@LbryAutomatedTestChannel"
|
||||
channelkey := "7943FWPBHZES4dUcMXSpDYwoM5a2tsyJT1R8V54QoUhekGcqmeH3hbzDXoLLQ8" +
|
||||
"oKkfb99PgGK5efrZeYqaxg4X5XRJMJ6gKC8hqKcnwhYkmKDXmoBDNgd2ccZ9jhP8z" +
|
||||
"HG3NJorAN9Hh4XMyBc5goBLZYYvC9MYvBmT3Fcteb5saqMvmQxFURv74NqXLQZC1t" +
|
||||
"p6iRZKfTj77Pd5gsBsCYAbVmCqzbm5m1hHkUmfFEZVGcQNTYCDwZn543xSMYvSPnJ" +
|
||||
"zt8tRYCJWaPdj713uENZZMo3gxuAMb1NwSnx8tbwETp7WPkpFLL6HZ9jKpB8BURHM" +
|
||||
"F1RFD1PRyqbC6YezPyPQ2oninKKHdBduvXZG5KF2G2Q3ixsuE2ntifBBo1f5PotRk" +
|
||||
"UanXKEafWxvXAayJjpsmZ4bFt7n6Xg4438WZXBiZKCPobLJAiHfe72n618kE6PCNU" +
|
||||
"77cyU5Rk8J3CuY6QzZPzwuiXz2GLfkUMCYd9jGT6g53XbE6SwCsmGnd9NJkBAaJf5" +
|
||||
"1FAYRURrhHnp79PAoHftEWtZEuU8MCPMdSRjzxYMRS4ScUzg5viDMTAkE8frsfCVZ" +
|
||||
"hxsFwGUyNNno8eiqrrYmpbJGEwwK3S4437JboAUEFPdMNn8zNQWZcLLVrK9KyQeKM" +
|
||||
"XpKkf4zJV6sZJ7gBMpzvPL18ULEgXTy7VsNBKmsfC1rM4WVG9ri1UixEcLDS79foC" +
|
||||
"Jb3FnSr1T4MRKESeN3W"
|
||||
response, err := d.ChannelImport(channelkey, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
channels, err := d.ChannelList(nil, 1, 50, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
seen := false
|
||||
for _, c := range channels.Items {
|
||||
if c.Name == channelName {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
t.Error("couldn't find imported channel")
|
||||
}
|
||||
t.Log("Response:", *response)
|
||||
}
|
||||
|
||||
func TestClient_ChannelImportWithWalletID(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
wallet, err := d.WalletCreate(id, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// A channel created just for automated testing purposes
|
||||
channelName := "@LbryAutomatedTestChannel"
|
||||
channelKey := "7943FWPBHZES4dUcMXSpDYwoM5a2tsyJT1R8V54QoUhekGcqmeH3hbzDXoLLQ8" +
|
||||
"oKkfb99PgGK5efrZeYqaxg4X5XRJMJ6gKC8hqKcnwhYkmKDXmoBDNgd2ccZ9jhP8z" +
|
||||
"HG3NJorAN9Hh4XMyBc5goBLZYYvC9MYvBmT3Fcteb5saqMvmQxFURv74NqXLQZC1t" +
|
||||
"p6iRZKfTj77Pd5gsBsCYAbVmCqzbm5m1hHkUmfFEZVGcQNTYCDwZn543xSMYvSPnJ" +
|
||||
"zt8tRYCJWaPdj713uENZZMo3gxuAMb1NwSnx8tbwETp7WPkpFLL6HZ9jKpB8BURHM" +
|
||||
"F1RFD1PRyqbC6YezPyPQ2oninKKHdBduvXZG5KF2G2Q3ixsuE2ntifBBo1f5PotRk" +
|
||||
"UanXKEafWxvXAayJjpsmZ4bFt7n6Xg4438WZXBiZKCPobLJAiHfe72n618kE6PCNU" +
|
||||
"77cyU5Rk8J3CuY6QzZPzwuiXz2GLfkUMCYd9jGT6g53XbE6SwCsmGnd9NJkBAaJf5" +
|
||||
"1FAYRURrhHnp79PAoHftEWtZEuU8MCPMdSRjzxYMRS4ScUzg5viDMTAkE8frsfCVZ" +
|
||||
"hxsFwGUyNNno8eiqrrYmpbJGEwwK3S4437JboAUEFPdMNn8zNQWZcLLVrK9KyQeKM" +
|
||||
"XpKkf4zJV6sZJ7gBMpzvPL18ULEgXTy7VsNBKmsfC1rM4WVG9ri1UixEcLDS79foC" +
|
||||
"Jb3FnSr1T4MRKESeN3W"
|
||||
response, err := d.ChannelImport(channelKey, &wallet.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
channels, err := d.ChannelList(nil, 1, 50, &wallet.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
seen := false
|
||||
for _, c := range channels.Items {
|
||||
if c.Name == channelName {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
t.Error("couldn't find imported channel")
|
||||
}
|
||||
t.Log("Response:", *response)
|
||||
}
|
||||
|
||||
func TestClient_WalletCreate(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
wallet, err := d.WalletCreate(id, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.ID != id {
|
||||
prettyPrint(*wallet)
|
||||
t.Fatalf("wallet ID mismatch, expected %q, got %q", id, wallet.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_WalletCreateWithOpts(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
wallet, err := d.WalletCreate(id, &WalletCreateOpts{CreateAccount: true, SingleKey: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accounts, err := d.AccountListForWallet(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prettyPrint(wallet)
|
||||
prettyPrint(accounts)
|
||||
if accounts.Items[0].Name == "" {
|
||||
t.Fatalf("account name is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_WalletList(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
_, err := d.WalletList(id, 1, 20)
|
||||
if err == nil {
|
||||
t.Fatalf("wallet %v was unexpectedly found", id)
|
||||
}
|
||||
derr, ok := err.(Error)
|
||||
if !ok {
|
||||
t.Fatalf("unknown error returned: %s", err)
|
||||
}
|
||||
if derr.Name != ErrorWalletNotLoaded {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.WalletCreate(id, &WalletCreateOpts{CreateAccount: true, SingleKey: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wList, err := d.WalletList(id, 1, 20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wList.Items) < 1 {
|
||||
t.Fatal("wallet list is empty")
|
||||
}
|
||||
if (wList.Items)[0].ID != id {
|
||||
t.Fatalf("wallet ID mismatch, expected %q, got %q", id, (wList.Items)[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_WalletRemoveWalletAdd(t *testing.T) {
|
||||
d := NewClient("")
|
||||
|
||||
id := "lbry#wallet#id:" + fmt.Sprintf("%d", rand.Int())
|
||||
wallet, err := d.WalletCreate(id, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.WalletRemove(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addedWallet, err := d.WalletAdd(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addedWallet.ID != wallet.ID {
|
||||
prettyPrint(*addedWallet)
|
||||
t.Fatalf("wallet ID mismatch, expected %q, got %q", wallet.ID, addedWallet.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_TransactionSummary(t *testing.T) {
|
||||
d := NewClient("https://api.na-backend.odysee.com/api/v1/proxy")
|
||||
r, err := d.TransactionShow("d104a1616c6af581e2046819de678f370d624e97cf176f95acaec4b183a42db6")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(r.Outputs) != 2 {
|
||||
t.Fatal("found wrong transaction")
|
||||
}
|
||||
if r.Outputs[0].Amount != "5.0" {
|
||||
t.Error("found wrong lbc amount for transaction.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/stream"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/stream"
|
||||
|
||||
schema "github.com/lbryio/lbryschema.go/claim"
|
||||
schema "github.com/lbryio/lbry.go/v2/schema/stake"
|
||||
lbryschema "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
@ -32,28 +33,42 @@ type Fee struct {
|
|||
}
|
||||
|
||||
type File struct {
|
||||
AddedOn int64 `json:"added_on"`
|
||||
BlobsCompleted uint64 `json:"blobs_completed"`
|
||||
BlobsInStream uint64 `json:"blobs_in_stream"`
|
||||
BlobsRemaining uint64 `json:"blobs_remaining"`
|
||||
ChannelClaimID string `json:"channel_claim_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ClaimID string `json:"claim_id"`
|
||||
ClaimName string `json:"claim_name"`
|
||||
Completed bool `json:"completed"`
|
||||
Confirmations int64 `json:"confirmations"`
|
||||
ContentFee *Fee `json:"content_fee"`
|
||||
DownloadDirectory string `json:"download_directory"`
|
||||
DownloadPath string `json:"download_path"`
|
||||
FileName string `json:"file_name"`
|
||||
Height int `json:"height"`
|
||||
IsFullyReflected bool `json:"is_fully_reflected"`
|
||||
Key string `json:"key"`
|
||||
Message string `json:"message"`
|
||||
Metadata *lbryschema.Claim `json:"protobuf"`
|
||||
Value *lbryschema.Claim `json:"protobuf"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Name string `json:"name"`
|
||||
Nout int `json:"nout"`
|
||||
Outpoint string `json:"outpoint"`
|
||||
PointsPaid decimal.Decimal `json:"points_paid"`
|
||||
PurchaseReceipt interface{} `json:"purchase_receipt"`
|
||||
ReflectorProgress int `json:"reflector_progress"`
|
||||
SdHash string `json:"sd_hash"`
|
||||
Status string `json:"status"`
|
||||
Stopped bool `json:"stopped"`
|
||||
StreamHash string `json:"stream_hash"`
|
||||
StreamName string `json:"stream_name"`
|
||||
StreamingURL string `json:"streaming_url"`
|
||||
SuggestedFileName string `json:"suggested_file_name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
TotalBytesLowerBound uint64 `json:"total_bytes_lower_bound"`
|
||||
Txid string `json:"txid"`
|
||||
UploadingToReflector bool `json:"uploading_to_reflector"`
|
||||
WrittenBytes uint64 `json:"written_bytes"`
|
||||
ChannelName *string `json:"channel_name,omitempty"`
|
||||
HasSignature *bool `json:"has_signature,omitempty"`
|
||||
SignatureIsValid *bool `json:"signature_is_valid,omitempty"`
|
||||
}
|
||||
|
||||
func getEnumVal(enum map[string]int32, data interface{}) (int32, error) {
|
||||
|
@ -166,22 +181,13 @@ type StreamAvailabilityResponse struct {
|
|||
}
|
||||
|
||||
type GetResponse File
|
||||
type FileListResponse []File
|
||||
|
||||
type WalletListResponse []string
|
||||
|
||||
type BlobAnnounceResponse bool
|
||||
|
||||
type WalletPrefillAddressesResponse struct {
|
||||
Broadcast bool `json:"broadcast"`
|
||||
Complete bool `json:"complete"`
|
||||
Hex string `json:"hex"`
|
||||
type FileListResponse struct {
|
||||
Items []File `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type WalletNewAddressResponse string
|
||||
|
||||
type WalletUnusedAddressResponse string
|
||||
|
||||
type Account struct {
|
||||
AddressGenerator struct {
|
||||
Change struct {
|
||||
|
@ -199,46 +205,57 @@ type Account struct {
|
|||
Encrypted bool `json:"encrypted"`
|
||||
ID string `json:"id"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Ledger *string `json:"ledger,omitempty"`
|
||||
ModifiedOn *float64 `json:"modified_on,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Preferences *struct {
|
||||
Theme string `json:"theme"`
|
||||
} `json:"preferences,omitempty"`
|
||||
PrivateKey *string `json:"private_key,omitempty"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Seed *string `json:"seed,omitempty"`
|
||||
Satoshis uint64 `json:"satoshis"`
|
||||
}
|
||||
|
||||
type AccountListResponse struct {
|
||||
LBCMainnet []Account `json:"lbc_mainnet"`
|
||||
LBCTestnet []Account `json:"lbc_testnet"`
|
||||
LBCRegtest []Account `json:"lbc_regtest"`
|
||||
Items []Account `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type AccountBalanceResponse string
|
||||
|
||||
type AccountCreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Seed string `json:"seed"`
|
||||
Ledger string `json:"ledger"`
|
||||
ModifiedOn float64 `json:"modified_on"`
|
||||
type AccountBalanceResponse struct {
|
||||
Available decimal.Decimal `json:"available"`
|
||||
Reserved decimal.Decimal `json:"reserved"`
|
||||
ReservedSubtotals struct {
|
||||
Claims decimal.Decimal `json:"claims"`
|
||||
Supports decimal.Decimal `json:"supports"`
|
||||
Tips decimal.Decimal `json:"tips"`
|
||||
} `json:"reserved_subtotals"`
|
||||
Total decimal.Decimal `json:"total"`
|
||||
}
|
||||
|
||||
type AccountRemoveResponse AccountCreateResponse
|
||||
|
||||
type Transaction struct {
|
||||
Address string `json:"address"`
|
||||
Amount string `json:"amount"`
|
||||
ClaimID string `json:"claim_id"`
|
||||
ClaimOp string `json:"claim_op"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
HasSigningKey bool `json:"has_signing_key"`
|
||||
Height int `json:"height"`
|
||||
IsChange bool `json:"is_change"`
|
||||
IsMine bool `json:"is_mine"`
|
||||
IsInternalTransfer bool `json:"is_internal_transfer"`
|
||||
IsMyInput bool `json:"is_my_input"`
|
||||
IsMyOutput bool `json:"is_my_output"`
|
||||
IsSpent bool `json:"is_spent"`
|
||||
Name string `json:"name"`
|
||||
NormalizedName string `json:"normalized_name"`
|
||||
Nout uint64 `json:"nout"`
|
||||
PermanentUrl string `json:"permanent_url"`
|
||||
Protobuf string `json:"protobuf,omitempty"`
|
||||
SigningChannel *Claim `json:"signing_channel,omitempty"`
|
||||
TimeStamp uint64 `json:"time_stamp"`
|
||||
Txid string `json:"txid"`
|
||||
Type string `json:"type"`
|
||||
Value *lbryschema.Claim `json:"protobuf"`
|
||||
Value *lbryschema.Claim `json:"protobuf,omitempty"`
|
||||
}
|
||||
|
||||
type TransactionSummary struct {
|
||||
|
@ -247,6 +264,7 @@ type TransactionSummary struct {
|
|||
Inputs []Transaction `json:"inputs"`
|
||||
Outputs []Transaction `json:"outputs"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
TotalInput string `json:"total_input"`
|
||||
TotalOutput string `json:"total_output"`
|
||||
Txid string `json:"txid"`
|
||||
}
|
||||
|
@ -255,7 +273,20 @@ type AccountFundResponse TransactionSummary
|
|||
|
||||
type Address string
|
||||
type AddressUnusedResponse Address
|
||||
type AddressListResponse []Address
|
||||
type AddressListResponse struct {
|
||||
Items []struct {
|
||||
Account string `json:"account"`
|
||||
Address Address `json:"address"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
UsedTimes uint64 `json:"used_times"`
|
||||
} `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type ChannelExportResponse string
|
||||
type ChannelImportResponse string
|
||||
|
||||
type ChannelListResponse struct {
|
||||
Items []Transaction `json:"items"`
|
||||
|
@ -274,32 +305,46 @@ type Support struct {
|
|||
Txid string `json:"txid"`
|
||||
}
|
||||
|
||||
type PurchaseReceipt struct {
|
||||
Address string `json:"file_name"`
|
||||
Amount string `json:"amount"`
|
||||
ClaimID string `json:"claim_id"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Height int `json:"height"`
|
||||
Nout uint64 `json:"nout"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Txid string `json:"txid"`
|
||||
Type string `json:"purchase"`
|
||||
}
|
||||
|
||||
type Claim struct {
|
||||
Address string `json:"address"`
|
||||
Amount string `json:"amount"`
|
||||
CanonicalURL string `json:"canonical_url"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ClaimID string `json:"claim_id"`
|
||||
ClaimOp string `json:"claim_op,omitempty"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Height int `json:"height"`
|
||||
IsChange bool `json:"is_change,omitempty"`
|
||||
IsChannelSignatureValid bool `json:"is_channel_signature_valid,omitempty"`
|
||||
IsMine bool `json:"is_mine,omitempty"`
|
||||
IsInternalTransfer bool `json:"is_internal_transfer"`
|
||||
IsMyInput bool `json:"is_my_input"`
|
||||
IsMyOutput bool `json:"is_my_output"`
|
||||
IsSpent bool `json:"is_spent"`
|
||||
Meta Meta `json:"meta,omitempty"`
|
||||
Name string `json:"name"`
|
||||
NormalizedName string `json:"normalized_name"`
|
||||
Nout uint64 `json:"nout"`
|
||||
PermanentURL string `json:"permanent_url"`
|
||||
SigningChannel struct {
|
||||
ClaimID string `json:"claim_id"`
|
||||
Name string `json:"name"`
|
||||
Value struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Title string `json:"title"`
|
||||
} `json:"value"`
|
||||
} `json:"signing_channel,omitempty"`
|
||||
PurchaseReceipt *PurchaseReceipt `json:"purchase_receipt,omitempty"`
|
||||
ShortURL string `json:"short_url"`
|
||||
SigningChannel *Claim `json:"signing_channel,omitempty"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Txid string `json:"txid"`
|
||||
Type string `json:"type,omitempty"`
|
||||
ValueType string `json:"value_type,omitempty"`
|
||||
Value lbryschema.Claim `json:"protobuf,omitempty"`
|
||||
ValueType string `json:"value_type,omitempty"`
|
||||
AbsoluteChannelPosition int `json:"absolute_channel_position,omitempty"`
|
||||
ChannelName string `json:"channel_name,omitempty"`
|
||||
ClaimSequence int64 `json:"claim_sequence,omitempty"`
|
||||
|
@ -311,17 +356,31 @@ type Claim struct {
|
|||
ValidAtHeight int `json:"valid_at_height,omitempty"`
|
||||
}
|
||||
|
||||
const reflectorURL = "http://blobs.lbry.io/"
|
||||
type Meta struct {
|
||||
ActivationHeight int64 `json:"activation_height,omitempty"`
|
||||
CreationHeight int64 `json:"creation_height,omitempty"`
|
||||
CreationTimestamp int `json:"creation_timestamp,omitempty"`
|
||||
EffectiveAmount string `json:"effective_amount,omitempty"`
|
||||
ExpirationHeight int64 `json:"expiration_height,omitempty"`
|
||||
IsControlling bool `json:"is_controlling,omitempty"`
|
||||
SupportAmount string `json:"support_amount,omitempty"`
|
||||
TrendingGlobal float64 `json:"trending_global,omitempty"`
|
||||
TrendingGroup float64 `json:"trending_group,omitempty"`
|
||||
TrendingLocal float64 `json:"trending_local,omitempty"`
|
||||
TrendingMixed float64 `json:"trending_mixed,omitempty"`
|
||||
}
|
||||
|
||||
const coldStorageURL = "https://s3.wasabisys.com/blobs.lbry.com/"
|
||||
|
||||
// GetStreamSizeByMagic uses "magic" to not just estimate, but actually return the exact size of a stream
|
||||
// It does so by fetching the sd blob and the last blob from our S3 bucket, decrypting and unpadding the last blob
|
||||
// adding up all full blobs that have a known size and finally adding the real last blob size too.
|
||||
// This will only work if we host at least the sd blob and the last blob on S3, if not, this will error.
|
||||
func (c *Claim) GetStreamSizeByMagic() (uint64, error) {
|
||||
func (c *Claim) GetStreamSizeByMagic() (streamSize uint64, e error) {
|
||||
if c.Value.GetStream() == nil {
|
||||
return 0, errors.Err("this claim is not a stream")
|
||||
}
|
||||
resp, err := http.Get(reflectorURL + hex.EncodeToString(c.Value.GetStream().Source.SdHash))
|
||||
resp, err := http.Get(coldStorageURL + hex.EncodeToString(c.Value.GetStream().Source.SdHash))
|
||||
if err != nil {
|
||||
return 0, errors.Err(err)
|
||||
}
|
||||
|
@ -340,12 +399,11 @@ func (c *Claim) GetStreamSizeByMagic() (uint64, error) {
|
|||
lastBlobIndex := len(sdb.BlobInfos) - 2
|
||||
lastBlobHash := sdb.BlobInfos[lastBlobIndex].BlobHash
|
||||
|
||||
var streamSize uint64 = 0
|
||||
if len(sdb.BlobInfos) > 2 {
|
||||
streamSize = uint64(stream.MaxBlobSize-1) * uint64(len(sdb.BlobInfos)-2)
|
||||
}
|
||||
|
||||
resp2, err := http.Get(reflectorURL + hex.EncodeToString(lastBlobHash))
|
||||
resp2, err := http.Get(coldStorageURL + hex.EncodeToString(lastBlobHash))
|
||||
if err != nil {
|
||||
return 0, errors.Err(err)
|
||||
}
|
||||
|
@ -355,7 +413,11 @@ func (c *Claim) GetStreamSizeByMagic() (uint64, error) {
|
|||
if err != nil {
|
||||
return 0, errors.Err(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
e = errors.Err("recovered from DecryptBlob panic for blob %s", lastBlobHash)
|
||||
}
|
||||
}()
|
||||
lastBlob, err := stream.DecryptBlob(body2, sdb.Key, sdb.BlobInfos[lastBlobIndex].IV)
|
||||
if err != nil {
|
||||
return 0, errors.Err(err)
|
||||
|
@ -365,6 +427,40 @@ func (c *Claim) GetStreamSizeByMagic() (uint64, error) {
|
|||
return streamSize, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ProtectedContentTag = SpecialContentType("c:members-only")
|
||||
PurchaseContentTag = SpecialContentType("c:purchase:")
|
||||
RentalContentTag = SpecialContentType("c:rental:")
|
||||
PreorderContentTag = SpecialContentType("c:preorder:")
|
||||
LegacyPurchaseContentTag = SpecialContentType("purchase:")
|
||||
LegacyRentalContentTag = SpecialContentType("rental:")
|
||||
LegacyPreorderContentTag = SpecialContentType("preorder:")
|
||||
ScheduledShowContentTag = SpecialContentType("c:scheduled:show")
|
||||
ScheduledHideContentTag = SpecialContentType("c:scheduled:hide")
|
||||
UnlistedContentTag = SpecialContentType("c:unlisted")
|
||||
)
|
||||
|
||||
type SpecialContentType string
|
||||
|
||||
//IsContentSpecial returns true if the claim is of a special content type
|
||||
func (c *Claim) IsContentSpecial(specialTags ...SpecialContentType) bool {
|
||||
for _, t := range c.Value.GetTags() {
|
||||
for _, ct := range specialTags {
|
||||
if strings.Contains(t, string(ct)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type StreamListResponse struct {
|
||||
Items []Claim `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type ClaimListResponse struct {
|
||||
Claims []Claim `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
|
@ -373,9 +469,24 @@ type ClaimListResponse struct {
|
|||
}
|
||||
type ClaimSearchResponse ClaimListResponse
|
||||
|
||||
type SupportListResponse struct {
|
||||
Items []Claim
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
BlobManager struct {
|
||||
FinishedBlobs uint64 `json:"finished_blobs"`
|
||||
Connections struct {
|
||||
MaxIncomingMbs float64 `json:"max_incoming_mbs"`
|
||||
MaxOutgoingMbs float64 `json:"max_outgoing_mbs"`
|
||||
TotalIncomingMbs float64 `json:"total_incoming_mbs"`
|
||||
TotalOutgoingMbs float64 `json:"total_outgoing_mbs"`
|
||||
TotalReceived int64 `json:"total_received"`
|
||||
TotalSent int64 `json:"total_sent"`
|
||||
} `json:"connections"`
|
||||
FinishedBlobs int64 `json:"finished_blobs"`
|
||||
} `json:"blob_manager"`
|
||||
ConnectionStatus struct {
|
||||
Code string `json:"code"`
|
||||
|
@ -385,28 +496,33 @@ type StatusResponse struct {
|
|||
NodeID string `json:"node_id"`
|
||||
PeersInRoutingTable uint64 `json:"peers_in_routing_table"`
|
||||
} `json:"dht"`
|
||||
FfmpegStatus struct {
|
||||
AnalyzeAudioVolume bool `json:"analyze_audio_volume"`
|
||||
Available bool `json:"available"`
|
||||
Which string `json:"which"`
|
||||
} `json:"ffmpeg_status"`
|
||||
FileManager struct {
|
||||
ManagedFiles int64 `json:"managed_files"`
|
||||
} `json:"file_manager"`
|
||||
HashAnnouncer struct {
|
||||
AnnounceQueueSize uint64 `json:"announce_queue_size"`
|
||||
} `json:"hash_announcer"`
|
||||
InstallationID string `json:"installation_id"`
|
||||
IsFirstRun bool `json:"is_first_run"`
|
||||
IsRunning bool `json:"is_running"`
|
||||
SkippedComponents []string `json:"skipped_components"`
|
||||
StartupStatus struct {
|
||||
BlobManager bool `json:"blob_manager"`
|
||||
BlockchainHeaders bool `json:"blockchain_headers"`
|
||||
Database bool `json:"database"`
|
||||
Dht bool `json:"dht"`
|
||||
ExchangeRateManager bool `json:"exchange_rate_manager"`
|
||||
FileManager bool `json:"file_manager"`
|
||||
HashAnnouncer bool `json:"hash_announcer"`
|
||||
LibtorrentComponent bool `json:"libtorrent_component"`
|
||||
PeerProtocolServer bool `json:"peer_protocol_server"`
|
||||
StreamManager bool `json:"stream_manager"`
|
||||
Upnp bool `json:"upnp"`
|
||||
Wallet bool `json:"wallet"`
|
||||
WalletServerPayments bool `json:"wallet_server_payments"`
|
||||
} `json:"startup_status"`
|
||||
StreamManager struct {
|
||||
ManagedFiles int64 `json:"managed_files"`
|
||||
} `json:"stream_manager"`
|
||||
Upnp struct {
|
||||
AioupnpVersion string `json:"aioupnp_version"`
|
||||
DhtRedirectSet bool `json:"dht_redirect_set"`
|
||||
|
@ -414,26 +530,104 @@ type StatusResponse struct {
|
|||
Gateway string `json:"gateway"`
|
||||
PeerRedirectSet bool `json:"peer_redirect_set"`
|
||||
Redirects struct{} `json:"redirects"`
|
||||
}
|
||||
} `json:"upnp"`
|
||||
Wallet struct {
|
||||
BestBlochash string `json:"best_blockhash"`
|
||||
AvailableServers int `json:"available_servers"`
|
||||
BestBlockhash string `json:"best_blockhash"`
|
||||
Blocks int `json:"blocks"`
|
||||
BlocksBehind int `json:"blocks_behind"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Connected string `json:"connected"`
|
||||
ConnectedFeatures struct {
|
||||
DailyFee string `json:"daily_fee"`
|
||||
Description string `json:"description"`
|
||||
DonationAddress string `json:"donation_address"`
|
||||
GenesisHash string `json:"genesis_hash"`
|
||||
HashFunction string `json:"hash_function"`
|
||||
Hosts struct {
|
||||
} `json:"hosts"`
|
||||
PaymentAddress string `json:"payment_address"`
|
||||
ProtocolMax string `json:"protocol_max"`
|
||||
ProtocolMin string `json:"protocol_min"`
|
||||
Pruning interface{} `json:"pruning"`
|
||||
ServerVersion string `json:"server_version"`
|
||||
TrendingAlgorithm string `json:"trending_algorithm"`
|
||||
} `json:"connected_features"`
|
||||
HeadersSynchronizationProgress int `json:"headers_synchronization_progress"`
|
||||
KnownServers int `json:"known_servers"`
|
||||
Servers []struct {
|
||||
Availability bool `json:"availability"`
|
||||
Host string `json:"host"`
|
||||
Latency float64 `json:"latency"`
|
||||
Port int `json:"port"`
|
||||
} `json:"servers"`
|
||||
} `json:"wallet"`
|
||||
WalletServerPayments struct {
|
||||
MaxFee string `json:"max_fee"`
|
||||
Running bool `json:"running"`
|
||||
} `json:"wallet_server_payments"`
|
||||
}
|
||||
|
||||
type UTXOListResponse []struct {
|
||||
type UTXOListResponse struct {
|
||||
Items []struct {
|
||||
Address string `json:"address"`
|
||||
Amount string `json:"amount"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Height int `json:"height"`
|
||||
IsClaim bool `json:"is_claim"`
|
||||
IsCoinbase bool `json:"is_coinbase"`
|
||||
IsSupport bool `json:"is_support"`
|
||||
IsUpdate bool `json:"is_update"`
|
||||
IsInternalTransfer bool `json:"is_internal_transfer"`
|
||||
IsMyInput bool `json:"is_my_input"`
|
||||
IsMyOutput bool `json:"is_my_output"`
|
||||
IsSpent bool `json:"is_spent"`
|
||||
Nout int `json:"nout"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Txid string `json:"txid"`
|
||||
Type string `json:"type"`
|
||||
} `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type UTXOReleaseResponse *string
|
||||
|
||||
type transactionListBlob struct {
|
||||
Address string `json:"address"`
|
||||
Amount string `json:"amount"`
|
||||
BalanceDelta string `json:"balance_delta"`
|
||||
ClaimId string `json:"claim_id"`
|
||||
ClaimName string `json:"claim_name"`
|
||||
IsSpent bool `json:"is_spent"`
|
||||
Nout int `json:"nout"`
|
||||
}
|
||||
|
||||
//TODO: this repeats all the fields from transactionListBlob which doesn't make sense
|
||||
// but if i extend the type with transactionListBlob it doesn't fill the fields. does our unmarshaller crap out on these?
|
||||
type supportBlob struct {
|
||||
Address string `json:"address"`
|
||||
Amount string `json:"amount"`
|
||||
BalanceDelta string `json:"balance_delta"`
|
||||
ClaimId string `json:"claim_id"`
|
||||
ClaimName string `json:"claim_name"`
|
||||
IsSpent bool `json:"is_spent"`
|
||||
IsTip bool `json:"is_tip"`
|
||||
Nout int `json:"nout"`
|
||||
}
|
||||
|
||||
type TransactionListResponse struct {
|
||||
Items []struct {
|
||||
AbandonInfo []transactionListBlob `json:"abandon_info"`
|
||||
ClaimInfo []transactionListBlob `json:"claim_info"`
|
||||
Confirmations int64 `json:"confirmations"`
|
||||
Date string `json:"date"`
|
||||
Fee string `json:"fee"`
|
||||
SupportInfo []supportBlob `json:"support_info"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Txid string `json:"txid"`
|
||||
UpdateInfo []transactionListBlob `json:"update_info"`
|
||||
Value string `json:"value"`
|
||||
} `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type VersionResponse struct {
|
||||
|
@ -451,25 +645,26 @@ type VersionResponse struct {
|
|||
} `json:"version_parts"`
|
||||
} `json:"distro"`
|
||||
LbrynetVersion string `json:"lbrynet_version"`
|
||||
LbryschemaVersion string `json:"lbryschema_version"`
|
||||
OsRelease string `json:"os_release"`
|
||||
OsSystem string `json:"os_system"`
|
||||
Platform string `json:"platform"`
|
||||
Processor string `json:"processor"`
|
||||
PythonVersion string `json:"python_version"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ResolveResponse map[string]ResolveResponseItem
|
||||
type ResolveResponseItem struct {
|
||||
Certificate *Claim `json:"certificate,omitempty"`
|
||||
Claim *Claim `json:"claim,omitempty"`
|
||||
ClaimsInChannel *uint64 `json:"claims_in_channel,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type NumClaimsInChannelResponse map[string]struct {
|
||||
ClaimsInChannel *uint64 `json:"claims_in_channel,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
type ResolveResponse map[string]Claim
|
||||
|
||||
type ClaimShowResponse *Claim
|
||||
|
||||
type Wallet struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WalletList struct {
|
||||
Items []Wallet `json:"items"`
|
||||
Page uint64 `json:"page"`
|
||||
PageSize uint64 `json:"page_size"`
|
||||
TotalPages uint64 `json:"total_pages"`
|
||||
}
|
||||
|
|
|
@ -10,49 +10,150 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultServerAddress = "https://api.odysee.tv"
|
||||
timeout = 5 * time.Second
|
||||
headerForwardedFor = "X-Forwarded-For"
|
||||
|
||||
userObjectPath = "user"
|
||||
userMeMethod = "me"
|
||||
userHasVerifiedEmailMethod = "has_verified_email"
|
||||
)
|
||||
|
||||
// Client stores data about internal-apis call it is about to make.
|
||||
type Client struct {
|
||||
ServerAddress string
|
||||
AuthToken string
|
||||
OAuthToken oauth2.TokenSource
|
||||
Logger *log.Logger
|
||||
serverAddress string
|
||||
extraHeaders map[string]string
|
||||
}
|
||||
|
||||
// ClientOpts allow to provide extra parameters to NewClient:
|
||||
// - ServerAddress
|
||||
// - RemoteIP — to forward the IP of a frontend client making the request
|
||||
type ClientOpts struct {
|
||||
ServerAddress string
|
||||
RemoteIP string
|
||||
}
|
||||
|
||||
// APIResponse reflects internal-apis JSON response format.
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error *string `json:"error"`
|
||||
Data *ResponseData `json:"data"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type data struct {
|
||||
obj map[string]interface{}
|
||||
array []interface{}
|
||||
}
|
||||
|
||||
func (d data) IsObject() bool {
|
||||
return d.obj != nil
|
||||
}
|
||||
|
||||
func (d data) IsArray() bool {
|
||||
return d.array != nil
|
||||
}
|
||||
|
||||
func (d data) Object() (map[string]interface{}, error) {
|
||||
if d.obj == nil {
|
||||
return nil, errors.New("no object data found")
|
||||
}
|
||||
return d.obj, nil
|
||||
}
|
||||
|
||||
func (d data) Array() ([]interface{}, error) {
|
||||
if d.array == nil {
|
||||
return nil, errors.New("no array data found")
|
||||
}
|
||||
return d.array, nil
|
||||
}
|
||||
|
||||
// APIError wraps errors returned by LBRY API server to discern them from other kinds (like http errors).
|
||||
type APIError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return fmt.Sprintf("api error: %v", e.Err)
|
||||
}
|
||||
|
||||
// ResponseData is a map containing parsed json response.
|
||||
type ResponseData map[string]interface{}
|
||||
type ResponseData interface {
|
||||
IsObject() bool
|
||||
IsArray() bool
|
||||
Object() (map[string]interface{}, error)
|
||||
Array() ([]interface{}, error)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultAPIHost = "https://api.lbry.com"
|
||||
timeout = 5 * time.Second
|
||||
userObjectPath = "user"
|
||||
)
|
||||
func makeMethodPath(obj, method string) string {
|
||||
return fmt.Sprintf("/%s/%s", obj, method)
|
||||
}
|
||||
|
||||
// NewClient returns a client instance for internal-apis. It requires authToken to be provided
|
||||
// for authentication.
|
||||
func NewClient(authToken string) Client {
|
||||
return Client{
|
||||
ServerAddress: defaultAPIHost,
|
||||
func NewClient(authToken string, opts *ClientOpts) Client {
|
||||
c := Client{
|
||||
serverAddress: defaultServerAddress,
|
||||
extraHeaders: make(map[string]string),
|
||||
AuthToken: authToken,
|
||||
Logger: log.StandardLogger(),
|
||||
}
|
||||
if opts != nil {
|
||||
if opts.ServerAddress != "" {
|
||||
c.serverAddress = opts.ServerAddress
|
||||
}
|
||||
if opts.RemoteIP != "" {
|
||||
c.extraHeaders[headerForwardedFor] = opts.RemoteIP
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NewOauthClient returns a client instance for internal-apis. It requires Oauth Token Source to be provided
|
||||
// for authentication.
|
||||
func NewOauthClient(token oauth2.TokenSource, opts *ClientOpts) Client {
|
||||
c := Client{
|
||||
serverAddress: defaultServerAddress,
|
||||
extraHeaders: make(map[string]string),
|
||||
OAuthToken: token,
|
||||
Logger: log.StandardLogger(),
|
||||
}
|
||||
if opts != nil {
|
||||
if opts.ServerAddress != "" {
|
||||
c.serverAddress = opts.ServerAddress
|
||||
}
|
||||
if opts.RemoteIP != "" {
|
||||
c.extraHeaders[headerForwardedFor] = opts.RemoteIP
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Client) getEndpointURL(object, method string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", c.ServerAddress, object, method)
|
||||
return fmt.Sprintf("%s%s", c.serverAddress, makeMethodPath(object, method))
|
||||
}
|
||||
|
||||
func (c Client) getEndpointURLFromPath(path string) string {
|
||||
return fmt.Sprintf("%s%s", c.serverAddress, path)
|
||||
}
|
||||
|
||||
func (c Client) prepareParams(params map[string]interface{}) (string, error) {
|
||||
form := url.Values{}
|
||||
if c.AuthToken != "" {
|
||||
form.Add("auth_token", c.AuthToken)
|
||||
} else if c.OAuthToken == nil {
|
||||
return "", errors.New("oauth token source must be supplied")
|
||||
}
|
||||
for k, v := range params {
|
||||
if k == "auth_token" {
|
||||
return "", errors.New("extra auth_token supplied in request params")
|
||||
|
@ -69,43 +170,100 @@ func (c Client) doCall(url string, payload string) ([]byte, error) {
|
|||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
if c.OAuthToken != nil {
|
||||
t, err := c.OAuthToken.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Type() != "Bearer" {
|
||||
return nil, errors.New("internal-apis requires an oAuth token of type 'Bearer'")
|
||||
}
|
||||
t.SetAuthHeader(req)
|
||||
}
|
||||
|
||||
for k, v := range c.extraHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
if r.StatusCode >= 500 {
|
||||
return body, fmt.Errorf("server returned non-OK status: %v", r.StatusCode)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
return ioutil.ReadAll(r.Body)
|
||||
}
|
||||
|
||||
// Call calls a remote internal-apis server, returning a response,
|
||||
// CallResource calls a remote internal-apis server resource, returning a response,
|
||||
// wrapped into standardized API Response struct.
|
||||
func (c Client) Call(object, method string, params map[string]interface{}) (ResponseData, error) {
|
||||
var rd ResponseData
|
||||
func (c Client) CallResource(object, method string, params map[string]interface{}) (ResponseData, error) {
|
||||
var d data
|
||||
payload, err := c.prepareParams(params)
|
||||
if err != nil {
|
||||
return rd, err
|
||||
return d, err
|
||||
}
|
||||
|
||||
body, err := c.doCall(c.getEndpointURL(object, method), payload)
|
||||
if err != nil {
|
||||
return rd, err
|
||||
return d, err
|
||||
}
|
||||
var ar APIResponse
|
||||
err = json.Unmarshal(body, &ar)
|
||||
if err != nil {
|
||||
return rd, err
|
||||
return d, err
|
||||
}
|
||||
if !ar.Success {
|
||||
return rd, errors.New(*ar.Error)
|
||||
return d, APIError{errors.New(*ar.Error)}
|
||||
}
|
||||
return *ar.Data, err
|
||||
if v, ok := ar.Data.([]interface{}); ok {
|
||||
d.array = v
|
||||
} else if v, ok := ar.Data.(map[string]interface{}); ok {
|
||||
d.obj = v
|
||||
}
|
||||
return d, err
|
||||
}
|
||||
|
||||
// UserMe returns user details for the user associated with the current auth_token
|
||||
func (c Client) UserMe() (ResponseData, error) {
|
||||
return c.Call(userObjectPath, "me", map[string]interface{}{})
|
||||
// Call calls a remote internal-apis server, returning a response,
|
||||
// wrapped into standardized API Response struct.
|
||||
func (c Client) Call(path string, params map[string]interface{}) (ResponseData, error) {
|
||||
var d data
|
||||
payload, err := c.prepareParams(params)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
|
||||
body, err := c.doCall(c.getEndpointURLFromPath(path), payload)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
var ar APIResponse
|
||||
err = json.Unmarshal(body, &ar)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
if !ar.Success {
|
||||
return d, APIError{errors.New(*ar.Error)}
|
||||
}
|
||||
if v, ok := ar.Data.([]interface{}); ok {
|
||||
d.array = v
|
||||
} else if v, ok := ar.Data.(map[string]interface{}); ok {
|
||||
d.obj = v
|
||||
}
|
||||
return d, err
|
||||
}
|
||||
|
||||
// UserMe returns user details for the user associated with the current auth_token.
|
||||
func (c Client) UserMe() (ResponseData, error) {
|
||||
return c.CallResource(userObjectPath, userMeMethod, map[string]interface{}{})
|
||||
}
|
||||
|
||||
// UserHasVerifiedEmail calls has_verified_email method.
|
||||
func (c Client) UserHasVerifiedEmail() (ResponseData, error) {
|
||||
return c.CallResource(userObjectPath, userHasVerifiedEmailMethod, map[string]interface{}{})
|
||||
}
|
||||
|
|
|
@ -1,35 +1,142 @@
|
|||
package lbryinc
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserMeWrongToken(t *testing.T) {
|
||||
c := NewClient("abc")
|
||||
r, err := c.UserMe()
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "could not authenticate user", err.Error())
|
||||
assert.Nil(t, r)
|
||||
func launchDummyServer(lastReq **http.Request, path, response string, status int) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if lastReq != nil {
|
||||
*lastReq = &*r
|
||||
}
|
||||
authT := r.FormValue("auth_token")
|
||||
if authT == "" {
|
||||
accessT := r.Header.Get("Authorization")
|
||||
if accessT == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.URL.Path != path {
|
||||
fmt.Printf("path doesn't match: %v != %v", r.URL.Path, path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(response))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const dummyServerURL = "http://127.0.0.1:59999"
|
||||
func TestUserMe(t *testing.T) {
|
||||
ts := launchDummyServer(nil, makeMethodPath(userObjectPath, userMeMethod), userMeResponse, http.StatusOK)
|
||||
defer ts.Close()
|
||||
|
||||
func launchDummyServer() {
|
||||
s := &http.Server{
|
||||
Addr: "127.0.0.1:59999",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
response := []byte(`{
|
||||
c := NewClient("realToken", &ClientOpts{ServerAddress: ts.URL})
|
||||
r, err := c.UserMe()
|
||||
assert.Nil(t, err)
|
||||
robj, err := r.Object()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "user@lbry.tv", robj["primary_email"])
|
||||
}
|
||||
|
||||
func TestListFiltered(t *testing.T) {
|
||||
ts := launchDummyServer(nil, "/file/list_filtered", listFilteredResponse, http.StatusOK)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewClient("realToken", &ClientOpts{ServerAddress: ts.URL})
|
||||
r, err := c.CallResource("file", "list_filtered", map[string]interface{}{"with_claim_id": "true"})
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, r.IsArray())
|
||||
_, err = r.Array()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHasVerifiedEmail(t *testing.T) {
|
||||
ts := launchDummyServer(nil, makeMethodPath(userObjectPath, userHasVerifiedEmailMethod), userHasVerifiedEmailResponse, http.StatusOK)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewClient("realToken", &ClientOpts{ServerAddress: ts.URL})
|
||||
r, err := c.UserHasVerifiedEmail()
|
||||
assert.Nil(t, err)
|
||||
robj, err := r.Object()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.EqualValues(t, 12345, robj["user_id"])
|
||||
assert.Equal(t, true, robj["has_verified_email"])
|
||||
}
|
||||
|
||||
func TestUserHasVerifiedEmailOAuth(t *testing.T) {
|
||||
ts := launchDummyServer(nil, makeMethodPath(userObjectPath, userHasVerifiedEmailMethod), userHasVerifiedEmailResponse, http.StatusOK)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewOauthClient(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "Test-Access-Token"}), &ClientOpts{ServerAddress: ts.URL})
|
||||
r, err := c.UserHasVerifiedEmail()
|
||||
assert.Nil(t, err)
|
||||
robj, err := r.Object()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.EqualValues(t, 12345, robj["user_id"])
|
||||
assert.Equal(t, true, robj["has_verified_email"])
|
||||
}
|
||||
|
||||
func TestRemoteIP(t *testing.T) {
|
||||
var req *http.Request
|
||||
ts := launchDummyServer(&req, makeMethodPath(userObjectPath, userMeMethod), userMeResponse, http.StatusOK)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewClient("realToken", &ClientOpts{ServerAddress: ts.URL, RemoteIP: "8.8.8.8"})
|
||||
_, err := c.UserMe()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []string{"8.8.8.8"}, req.Header["X-Forwarded-For"])
|
||||
}
|
||||
|
||||
func TestWrongToken(t *testing.T) {
|
||||
c := NewClient("zcasdasc", nil)
|
||||
|
||||
r, err := c.UserHasVerifiedEmail()
|
||||
assert.False(t, r.IsObject())
|
||||
assert.EqualError(t, err, "api error: could not authenticate user")
|
||||
assert.ErrorAs(t, err, &APIError{})
|
||||
}
|
||||
|
||||
func TestHTTPError(t *testing.T) {
|
||||
c := NewClient("zcasdasc", &ClientOpts{ServerAddress: "http://lolcathost"})
|
||||
|
||||
r, err := c.UserHasVerifiedEmail()
|
||||
assert.False(t, r.IsObject())
|
||||
assert.EqualError(t, err, `Post "http://lolcathost/user/has_verified_email": dial tcp: lookup lolcathost: no such host`)
|
||||
}
|
||||
|
||||
func TestGatewayError(t *testing.T) {
|
||||
var req *http.Request
|
||||
ts := launchDummyServer(&req, makeMethodPath(userObjectPath, userHasVerifiedEmailMethod), "", http.StatusBadGateway)
|
||||
defer ts.Close()
|
||||
c := NewClient("zcasdasc", &ClientOpts{ServerAddress: ts.URL})
|
||||
|
||||
r, err := c.UserHasVerifiedEmail()
|
||||
assert.False(t, r.IsObject())
|
||||
assert.EqualError(t, err, `server returned non-OK status: 502`)
|
||||
}
|
||||
|
||||
const userMeResponse = `{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"data": {
|
||||
"id": 751365,
|
||||
"id": 12345,
|
||||
"language": "en",
|
||||
"given_name": null,
|
||||
"family_name": null,
|
||||
|
@ -40,26 +147,36 @@ func launchDummyServer() {
|
|||
"invites_remaining": 0,
|
||||
"invite_reward_claimed": false,
|
||||
"is_email_enabled": true,
|
||||
"manual_approval_user_id": 837139,
|
||||
"manual_approval_user_id": 654,
|
||||
"reward_status_change_trigger": "manual",
|
||||
"primary_email": "andrey@lbry.com",
|
||||
"primary_email": "user@lbry.tv",
|
||||
"has_verified_email": true,
|
||||
"is_identity_verified": false,
|
||||
"is_reward_approved": true,
|
||||
"groups": []
|
||||
}
|
||||
}`)
|
||||
w.Write(response)
|
||||
}),
|
||||
}
|
||||
log.Fatal(s.ListenAndServe())
|
||||
}
|
||||
}`
|
||||
|
||||
func TestUserMe(t *testing.T) {
|
||||
go launchDummyServer()
|
||||
c := NewClient("realToken")
|
||||
c.ServerAddress = dummyServerURL
|
||||
r, err := c.UserMe()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, r["primary_email"], "andrey@lbry.com")
|
||||
}
|
||||
const userHasVerifiedEmailResponse = `{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"data": {
|
||||
"user_id": 12345,
|
||||
"has_verified_email": true
|
||||
}
|
||||
}`
|
||||
|
||||
const listFilteredResponse = `{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"data": [
|
||||
{
|
||||
"claim_id": "322ce77e9085d9da42279c790f7c9755b4916fca",
|
||||
"outpoint": "20e04af21a569061ced7aa1801a43b4ed4839dfeb79919ea49a4059c7fe114c5:0"
|
||||
},
|
||||
{
|
||||
"claim_id": "61496c567badcd98b82d9a700a8d56fd8a5fa8fb",
|
||||
"outpoint": "657e4ec774524b326f9d3ecb9f468ea085bd1f3d450565f0330feca02e8fd25b:0"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
|
301
extras/orderedmap/ordered_map.go
Normal file
301
extras/orderedmap/ordered_map.go
Normal file
|
@ -0,0 +1,301 @@
|
|||
package orderedmap
|
||||
|
||||
// mostly from https://github.com/iancoleman/orderedmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
type keyIndex struct {
|
||||
Key string
|
||||
Index int
|
||||
}
|
||||
|
||||
type byIndex []keyIndex
|
||||
|
||||
func (a byIndex) Len() int { return len(a) }
|
||||
func (a byIndex) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byIndex) Less(i, j int) bool { return a[i].Index < a[j].Index }
|
||||
|
||||
type Map struct {
|
||||
l sync.RWMutex
|
||||
keys []string
|
||||
values map[string]interface{}
|
||||
}
|
||||
|
||||
func New() *Map {
|
||||
o := Map{}
|
||||
o.l = sync.RWMutex{}
|
||||
o.keys = []string{}
|
||||
o.values = map[string]interface{}{}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (o *Map) Get(key string) (interface{}, bool) {
|
||||
o.l.RLock()
|
||||
defer o.l.RUnlock()
|
||||
val, exists := o.values[key]
|
||||
return val, exists
|
||||
}
|
||||
|
||||
func (o *Map) Set(key string, value interface{}) {
|
||||
o.l.Lock()
|
||||
defer o.l.Unlock()
|
||||
_, exists := o.values[key]
|
||||
if !exists {
|
||||
o.keys = append(o.keys, key)
|
||||
}
|
||||
o.values[key] = value
|
||||
}
|
||||
|
||||
const outOfRange = "position value %d is outside of the range %d - %d"
|
||||
|
||||
//InsertAt This is a zero based position index: 0,1,2,3..n(from left) OR -1,-2,-3...-n(from right) where -1 is the last place.
|
||||
func (o *Map) InsertAt(key string, value interface{}, position int) error {
|
||||
o.l.Lock()
|
||||
defer o.l.Unlock()
|
||||
var index = position
|
||||
if position < 0 {
|
||||
// support indexing from the back: -1 = last in array.
|
||||
index += len(o.keys) + 1
|
||||
if index < 0 || index > len(o.keys) {
|
||||
return errors.Err(fmt.Sprintf(outOfRange, position, len(o.keys), len(o.keys)-1))
|
||||
}
|
||||
} else if index > len(o.keys) {
|
||||
return errors.Err(fmt.Sprintf(outOfRange, position, len(o.keys), len(o.keys)-1))
|
||||
}
|
||||
_, exists := o.values[key]
|
||||
if !exists {
|
||||
// left + key + right
|
||||
o.keys = append(o.keys[0:index], append([]string{key}, o.keys[index:]...)...)
|
||||
}
|
||||
o.values[key] = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Map) Prepend(key string, value interface{}) {
|
||||
o.l.Lock()
|
||||
defer o.l.Unlock()
|
||||
_, exists := o.values[key]
|
||||
if !exists {
|
||||
o.keys = append([]string{key}, o.keys...)
|
||||
}
|
||||
o.values[key] = value
|
||||
}
|
||||
|
||||
func (o *Map) Delete(key string) {
|
||||
o.l.Lock()
|
||||
defer o.l.Unlock()
|
||||
// check key is in use
|
||||
_, ok := o.values[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// remove from keys
|
||||
for i, k := range o.keys {
|
||||
if k == key {
|
||||
o.keys = append(o.keys[:i], o.keys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// remove from values
|
||||
delete(o.values, key)
|
||||
}
|
||||
|
||||
func (o *Map) Keys() []string {
|
||||
o.l.RLock()
|
||||
defer o.l.RUnlock()
|
||||
return o.keys
|
||||
}
|
||||
|
||||
func (o *Map) UnmarshalJSON(b []byte) error {
|
||||
o.l.Lock()
|
||||
defer o.l.Unlock()
|
||||
m := map[string]interface{}{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
s := string(b)
|
||||
mapToOrderedMap(o, s, m)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapToOrderedMap(o *Map, s string, m map[string]interface{}) {
|
||||
// Get the order of the keys
|
||||
orderedKeys := []keyIndex{}
|
||||
for k := range m {
|
||||
kEscaped := strings.Replace(k, `"`, `\"`, -1)
|
||||
kQuoted := `"` + kEscaped + `"`
|
||||
// Find how much content exists before this key.
|
||||
// If all content from this key and after is replaced with a close
|
||||
// brace, it should still form a valid json string.
|
||||
sTrimmed := s
|
||||
for len(sTrimmed) > 0 {
|
||||
lastIndex := strings.LastIndex(sTrimmed, kQuoted)
|
||||
if lastIndex == -1 {
|
||||
break
|
||||
}
|
||||
sTrimmed = sTrimmed[0:lastIndex]
|
||||
sTrimmed = strings.TrimSpace(sTrimmed)
|
||||
if len(sTrimmed) > 0 && sTrimmed[len(sTrimmed)-1] == ',' {
|
||||
sTrimmed = sTrimmed[0 : len(sTrimmed)-1]
|
||||
}
|
||||
maybeValidJson := sTrimmed + "}"
|
||||
testMap := map[string]interface{}{}
|
||||
err := json.Unmarshal([]byte(maybeValidJson), &testMap)
|
||||
if err == nil {
|
||||
// record the position of this key in s
|
||||
ki := keyIndex{
|
||||
Key: k,
|
||||
Index: len(sTrimmed),
|
||||
}
|
||||
orderedKeys = append(orderedKeys, ki)
|
||||
// shorten the string to get the next key
|
||||
startOfValueIndex := lastIndex + len(kQuoted)
|
||||
valueStr := s[startOfValueIndex : len(s)-1]
|
||||
valueStr = strings.TrimSpace(valueStr)
|
||||
if len(valueStr) > 0 && valueStr[0] == ':' {
|
||||
valueStr = valueStr[1:]
|
||||
}
|
||||
valueStr = strings.TrimSpace(valueStr)
|
||||
if valueStr[0] == '{' {
|
||||
// if the value for this key is a map, convert it to an orderedmap.
|
||||
// find end of valueStr by removing everything after last }
|
||||
// until it forms valid json
|
||||
hasValidJson := false
|
||||
i := 1
|
||||
for i < len(valueStr) && !hasValidJson {
|
||||
if valueStr[i] != '}' {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
subTestMap := map[string]interface{}{}
|
||||
testValue := valueStr[0 : i+1]
|
||||
err = json.Unmarshal([]byte(testValue), &subTestMap)
|
||||
if err == nil {
|
||||
hasValidJson = true
|
||||
valueStr = testValue
|
||||
break
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
// convert to orderedmap
|
||||
if hasValidJson {
|
||||
mkTyped := m[k].(map[string]interface{})
|
||||
oo := &Map{}
|
||||
mapToOrderedMap(oo, valueStr, mkTyped)
|
||||
m[k] = oo
|
||||
}
|
||||
} else if valueStr[0] == '[' {
|
||||
// if the value for this key is a []interface, convert any map items to an orderedmap.
|
||||
// find end of valueStr by removing everything after last ]
|
||||
// until it forms valid json
|
||||
hasValidJson := false
|
||||
i := 1
|
||||
for i < len(valueStr) && !hasValidJson {
|
||||
if valueStr[i] != ']' {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
subTestSlice := []interface{}{}
|
||||
testValue := valueStr[0 : i+1]
|
||||
err = json.Unmarshal([]byte(testValue), &subTestSlice)
|
||||
if err == nil {
|
||||
hasValidJson = true
|
||||
valueStr = testValue
|
||||
break
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if hasValidJson {
|
||||
itemsStr := valueStr[1 : len(valueStr)-1]
|
||||
// get next item in the slice
|
||||
itemIndex := 0
|
||||
startItem := 0
|
||||
endItem := 0
|
||||
for endItem < len(itemsStr) {
|
||||
if itemsStr[endItem] != ',' && endItem < len(itemsStr)-1 {
|
||||
endItem = endItem + 1
|
||||
continue
|
||||
}
|
||||
// if this substring compiles to json, it's the next item
|
||||
possibleItemStr := strings.TrimSpace(itemsStr[startItem:endItem])
|
||||
var possibleItem interface{}
|
||||
err = json.Unmarshal([]byte(possibleItemStr), &possibleItem)
|
||||
if err != nil {
|
||||
endItem = endItem + 1
|
||||
continue
|
||||
}
|
||||
// if item is map, convert to orderedmap
|
||||
if possibleItemStr[0] == '{' {
|
||||
mkTyped := m[k].([]interface{})
|
||||
mkiTyped := mkTyped[itemIndex].(map[string]interface{})
|
||||
oo := &Map{}
|
||||
mapToOrderedMap(oo, possibleItemStr, mkiTyped)
|
||||
// replace original map with orderedmap
|
||||
mkTyped[itemIndex] = oo
|
||||
m[k] = mkTyped
|
||||
}
|
||||
// remove this item from itemsStr
|
||||
startItem = endItem + 1
|
||||
endItem = endItem + 1
|
||||
itemIndex = itemIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the keys
|
||||
sort.Sort(byIndex(orderedKeys))
|
||||
// Convert sorted keys to string slice
|
||||
k := []string{}
|
||||
for _, ki := range orderedKeys {
|
||||
k = append(k, ki.Key)
|
||||
}
|
||||
// Set the Map values
|
||||
o.values = m
|
||||
o.keys = k
|
||||
}
|
||||
|
||||
func (o *Map) Copy() *Map {
|
||||
new := New()
|
||||
|
||||
for _, k := range o.keys {
|
||||
v, _ := o.Get(k)
|
||||
new.Set(k, v)
|
||||
}
|
||||
|
||||
return new
|
||||
}
|
||||
|
||||
func (o *Map) MarshalJSON() ([]byte, error) {
|
||||
o.l.RLock()
|
||||
defer o.l.RUnlock()
|
||||
s := "{"
|
||||
for _, k := range o.keys {
|
||||
// add key
|
||||
kEscaped := strings.Replace(k, `"`, `\"`, -1)
|
||||
s = s + `"` + kEscaped + `":`
|
||||
// add value
|
||||
v := o.values[k]
|
||||
vBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return []byte{}, errors.Err(err)
|
||||
}
|
||||
s = s + string(vBytes) + ","
|
||||
}
|
||||
if len(o.keys) > 0 {
|
||||
s = s[0 : len(s)-1]
|
||||
}
|
||||
s = s + "}"
|
||||
return []byte(s), nil
|
||||
}
|
478
extras/orderedmap/ordered_map_test.go
Normal file
478
extras/orderedmap/ordered_map_test.go
Normal file
|
@ -0,0 +1,478 @@
|
|||
package orderedmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func TestOrderedMap(t *testing.T) {
|
||||
o := New()
|
||||
// number
|
||||
o.Set("number", 3)
|
||||
v, _ := o.Get("number")
|
||||
if v.(int) != 3 {
|
||||
t.Error("Set number")
|
||||
}
|
||||
// string
|
||||
o.Set("string", "x")
|
||||
v, _ = o.Get("string")
|
||||
if v.(string) != "x" {
|
||||
t.Error("Set string")
|
||||
}
|
||||
// string slice
|
||||
o.Set("strings", []string{
|
||||
"t",
|
||||
"u",
|
||||
})
|
||||
v, _ = o.Get("strings")
|
||||
if v.([]string)[0] != "t" {
|
||||
t.Error("Set strings first index")
|
||||
}
|
||||
if v.([]string)[1] != "u" {
|
||||
t.Error("Set strings second index")
|
||||
}
|
||||
// mixed slice
|
||||
o.Set("mixed", []interface{}{
|
||||
1,
|
||||
"1",
|
||||
})
|
||||
v, _ = o.Get("mixed")
|
||||
if v.([]interface{})[0].(int) != 1 {
|
||||
t.Error("Set mixed int")
|
||||
}
|
||||
if v.([]interface{})[1].(string) != "1" {
|
||||
t.Error("Set mixed string")
|
||||
}
|
||||
// overriding existing key
|
||||
o.Set("number", 4)
|
||||
v, _ = o.Get("number")
|
||||
if v.(int) != 4 {
|
||||
t.Error("Override existing key")
|
||||
}
|
||||
// Keys method
|
||||
keys := o.Keys()
|
||||
expectedKeys := []string{
|
||||
"number",
|
||||
"string",
|
||||
"strings",
|
||||
"mixed",
|
||||
}
|
||||
for i := range keys {
|
||||
if keys[i] != expectedKeys[i] {
|
||||
t.Error("Keys method", keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
for i := range expectedKeys {
|
||||
if keys[i] != expectedKeys[i] {
|
||||
t.Error("Keys method", keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
// delete
|
||||
o.Delete("strings")
|
||||
o.Delete("not a key being used")
|
||||
if len(o.Keys()) != 3 {
|
||||
t.Error("Delete method")
|
||||
}
|
||||
_, ok := o.Get("strings")
|
||||
if ok {
|
||||
t.Error("Delete did not remove 'strings' key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlankMarshalJSON(t *testing.T) {
|
||||
o := New()
|
||||
// blank map
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
t.Error("Marshalling blank map to json", err)
|
||||
}
|
||||
s := string(b)
|
||||
// check json is correctly ordered
|
||||
if s != `{}` {
|
||||
t.Error("JSON Marshaling blank map value is incorrect", s)
|
||||
}
|
||||
// convert to indented json
|
||||
bi, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
t.Error("Marshalling indented json for blank map", err)
|
||||
}
|
||||
si := string(bi)
|
||||
ei := `{}`
|
||||
if si != ei {
|
||||
fmt.Println(ei)
|
||||
fmt.Println(si)
|
||||
t.Error("JSON MarshalIndent blank map value is incorrect", si)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
o := New()
|
||||
// number
|
||||
o.Set("number", 3)
|
||||
// string
|
||||
o.Set("string", "x")
|
||||
// new value keeps key in old position
|
||||
o.Set("number", 4)
|
||||
// keys not sorted alphabetically
|
||||
o.Set("z", 1)
|
||||
o.Set("a", 2)
|
||||
o.Set("b", 3)
|
||||
// slice
|
||||
o.Set("slice", []interface{}{
|
||||
"1",
|
||||
1,
|
||||
})
|
||||
// orderedmap
|
||||
v := New()
|
||||
v.Set("e", 1)
|
||||
v.Set("a", 2)
|
||||
o.Set("orderedmap", v)
|
||||
// double quote in key
|
||||
o.Set(`test"ing`, 9)
|
||||
// convert to json
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
t.Error("Marshalling json", err)
|
||||
}
|
||||
s := string(b)
|
||||
// check json is correctly ordered
|
||||
if s != `{"number":4,"string":"x","z":1,"a":2,"b":3,"slice":["1",1],"orderedmap":{"e":1,"a":2},"test\"ing":9}` {
|
||||
t.Error("JSON Marshal value is incorrect", s)
|
||||
}
|
||||
// convert to indented json
|
||||
bi, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
t.Error("Marshalling indented json", err)
|
||||
}
|
||||
si := string(bi)
|
||||
ei := `{
|
||||
"number": 4,
|
||||
"string": "x",
|
||||
"z": 1,
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"slice": [
|
||||
"1",
|
||||
1
|
||||
],
|
||||
"orderedmap": {
|
||||
"e": 1,
|
||||
"a": 2
|
||||
},
|
||||
"test\"ing": 9
|
||||
}`
|
||||
if si != ei {
|
||||
fmt.Println(ei)
|
||||
fmt.Println(si)
|
||||
t.Error("JSON MarshalIndent value is incorrect", si)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalJSON(t *testing.T) {
|
||||
s := `{
|
||||
"number": 4,
|
||||
"string": "x",
|
||||
"z": 1,
|
||||
"a": "should not break with unclosed { character in value",
|
||||
"b": 3,
|
||||
"slice": [
|
||||
"1",
|
||||
1
|
||||
],
|
||||
"orderedmap": {
|
||||
"e": 1,
|
||||
"a { nested key with brace": "with a }}}} }} {{{ brace value",
|
||||
"after": {
|
||||
"link": "test {{{ with even deeper nested braces }"
|
||||
}
|
||||
},
|
||||
"test\"ing": 9,
|
||||
"after": 1,
|
||||
"multitype_array": [
|
||||
"test",
|
||||
1,
|
||||
{ "map": "obj", "it" : 5, ":colon in key": "colon: in value" }
|
||||
],
|
||||
"should not break with { character in key": 1
|
||||
}`
|
||||
o := New()
|
||||
err := json.Unmarshal([]byte(s), &o)
|
||||
if err != nil {
|
||||
t.Error("JSON Unmarshal error", err)
|
||||
}
|
||||
// Check the root keys
|
||||
expectedKeys := []string{
|
||||
"number",
|
||||
"string",
|
||||
"z",
|
||||
"a",
|
||||
"b",
|
||||
"slice",
|
||||
"orderedmap",
|
||||
"test\"ing",
|
||||
"after",
|
||||
"multitype_array",
|
||||
"should not break with { character in key",
|
||||
}
|
||||
k := o.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Unmarshal root key order", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
// Check nested maps are converted to orderedmaps
|
||||
// nested 1 level deep
|
||||
expectedKeys = []string{
|
||||
"e",
|
||||
"a { nested key with brace",
|
||||
"after",
|
||||
}
|
||||
vi, ok := o.Get("orderedmap")
|
||||
if !ok {
|
||||
t.Error("Missing key for nested map 1 deep")
|
||||
}
|
||||
v := vi.(*Map)
|
||||
k = v.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Key order for nested map 1 deep ", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
// nested 2 levels deep
|
||||
expectedKeys = []string{
|
||||
"link",
|
||||
}
|
||||
vi, ok = v.Get("after")
|
||||
if !ok {
|
||||
t.Error("Missing key for nested map 2 deep")
|
||||
}
|
||||
v = vi.(*Map)
|
||||
k = v.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
// multitype array
|
||||
expectedKeys = []string{
|
||||
"map",
|
||||
"it",
|
||||
":colon in key",
|
||||
}
|
||||
vislice, ok := o.Get("multitype_array")
|
||||
if !ok {
|
||||
t.Error("Missing key for multitype array")
|
||||
}
|
||||
vslice := vislice.([]interface{})
|
||||
vmap := vslice[2].(*Map)
|
||||
k = vmap.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalJSONSpecialChars(t *testing.T) {
|
||||
s := `{ " \\\\\\\\\\\\ " : { "\\\\\\" : "\\\\\"\\" }, "\\": " \\\\ test " }`
|
||||
o := New()
|
||||
err := json.Unmarshal([]byte(s), &o)
|
||||
if err != nil {
|
||||
t.Error("JSON Unmarshal error with special chars", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalJSONArrayOfMaps(t *testing.T) {
|
||||
s := `
|
||||
{
|
||||
"name": "test",
|
||||
"percent": 6,
|
||||
"breakdown": [
|
||||
{
|
||||
"name": "a",
|
||||
"percent": 0.9
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"percent": 0.9
|
||||
},
|
||||
{
|
||||
"name": "d",
|
||||
"percent": 0.4
|
||||
},
|
||||
{
|
||||
"name": "e",
|
||||
"percent": 2.7
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
o := New()
|
||||
err := json.Unmarshal([]byte(s), &o)
|
||||
if err != nil {
|
||||
t.Error("JSON Unmarshal error", err)
|
||||
}
|
||||
// Check the root keys
|
||||
expectedKeys := []string{
|
||||
"name",
|
||||
"percent",
|
||||
"breakdown",
|
||||
}
|
||||
k := o.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Unmarshal root key order", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
// Check nested maps are converted to orderedmaps
|
||||
// nested 1 level deep
|
||||
expectedKeys = []string{
|
||||
"name",
|
||||
"percent",
|
||||
}
|
||||
vi, ok := o.Get("breakdown")
|
||||
if !ok {
|
||||
t.Error("Missing key for nested map 1 deep")
|
||||
}
|
||||
vs := vi.([]interface{})
|
||||
for _, vInterface := range vs {
|
||||
v := vInterface.(*Map)
|
||||
k = v.Keys()
|
||||
for i := range k {
|
||||
if k[i] != expectedKeys[i] {
|
||||
t.Error("Key order for nested map 1 deep ", i, k[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertAt(t *testing.T) {
|
||||
om := New()
|
||||
om.Set("zero", 0)
|
||||
om.Set("one", 1)
|
||||
om.Set("two", 2)
|
||||
|
||||
err := om.InsertAt("TEST", 10000, 4) //3 is this added one in size of map
|
||||
if err == nil {
|
||||
t.Error("expected insert at greater position than size of map to produce error")
|
||||
}
|
||||
|
||||
err = om.InsertAt("A", 100, 2)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Test it's at end
|
||||
if om.values[om.keys[2]] != 100 {
|
||||
t.Error("expected entry A to be at position 2", om.keys)
|
||||
}
|
||||
if om.values[om.keys[3]] != 2 {
|
||||
t.Error("expected two to be in position 1", om.keys)
|
||||
}
|
||||
|
||||
err = om.InsertAt("B", 200, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if om.values[om.keys[0]] != 200 {
|
||||
t.Error("expected B to be position 0", om.keys)
|
||||
}
|
||||
|
||||
err = om.InsertAt("C", 300, -1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Should show up at the end
|
||||
if om.values[om.keys[len(om.keys)-1]] != 300 {
|
||||
t.Error(fmt.Sprintf("expected C to be in position %d", len(om.keys)-1), om.keys)
|
||||
}
|
||||
|
||||
err = om.InsertAt("D", 400, 1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if om.values[om.keys[1]] != 400 {
|
||||
t.Error("expceted D to be position 1", om.keys)
|
||||
}
|
||||
|
||||
err = om.InsertAt("F", 600, -8)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if om.values[om.keys[0]] != 600 {
|
||||
t.Error("expected F to be in position 0", om.keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrency(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
type concurrency struct {
|
||||
a string
|
||||
b int
|
||||
c time.Time
|
||||
d bool
|
||||
}
|
||||
|
||||
//Starting Map
|
||||
m := New()
|
||||
m.Set("A", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("B", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("C", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("D", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("E", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("F", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("G", concurrency{"string", 10, time.Now(), true})
|
||||
m.Set("H", concurrency{"string", 10, time.Now(), true})
|
||||
//Inserts
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
m.Set("New"+strconv.Itoa(index), concurrency{"string", index, time.Now(), cast.ToBool(index % 2)})
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
//Reads
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
_, _ = m.Get("New" + strconv.Itoa(rand.Intn(99)))
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
//Marshalling like endpoint
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
_, err := m.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
}
|
|
@ -8,8 +8,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/extras/null"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/null"
|
||||
)
|
||||
|
||||
func InterpolateParams(query string, args ...interface{}) (string, error) {
|
||||
|
@ -87,11 +87,11 @@ func InterpolateParams(query string, args ...interface{}) (string, error) {
|
|||
|
||||
// Qs is a shortcut for one group of positional placeholders
|
||||
func Qs(count int) string {
|
||||
return placeholders(false, count, 1, 1)
|
||||
return Placeholders(false, count, 1, 1)
|
||||
}
|
||||
|
||||
// placeholders creates indexed or positional placeholders, in groups, with different starts
|
||||
func placeholders(indexPlaceholders bool, count int, start int, group int) string {
|
||||
func Placeholders(indexPlaceholders bool, count int, start int, group int) string {
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
if start == 0 || group == 0 {
|
||||
|
|
|
@ -101,8 +101,6 @@ func (s *Group) DoneNamed(name string) {
|
|||
|
||||
if s.waitingOn != nil {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if current, ok := s.waitingOn[name]; ok {
|
||||
if current <= 1 {
|
||||
delete(s.waitingOn, name)
|
||||
|
@ -112,9 +110,16 @@ func (s *Group) DoneNamed(name string) {
|
|||
} else {
|
||||
log.Printf("%s is not recorded in stop group map", name)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.listWaitingOn()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Group) listWaitingOn() {
|
||||
if s.waitingOn != nil {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
log.Printf("-->> LIST ROUTINES WAITING ON")
|
||||
|
||||
for k, v := range s.waitingOn {
|
||||
if v > 0 {
|
||||
log.Printf("waiting on %d %s routines...", v, k)
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
func publicKey(isPrivateRepo bool) (*rsa.PublicKey, error) {
|
||||
|
|
|
@ -6,7 +6,7 @@ package util
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/null"
|
||||
"github.com/lbryio/lbry.go/v2/extras/null"
|
||||
)
|
||||
|
||||
func PtrToBool(b bool) *bool { return &b }
|
||||
|
@ -33,6 +33,9 @@ func PtrToUintptr(u uintptr) *uintptr { return &u }
|
|||
|
||||
func PtrToNullString(s string) *null.String { n := null.StringFrom(s); return &n }
|
||||
func PtrToNullUint64(u uint64) *null.Uint64 { n := null.Uint64From(u); return &n }
|
||||
func PtrToNullTime(t time.Time) *null.Time { n := null.TimeFrom(t); return &n }
|
||||
func PtrToNullFloat64(f float64) *null.Float64 { n := null.Float64From(f); return &n }
|
||||
func PtrToNullFloat32(f float32) *null.Float32 { n := null.Float32From(f); return &n }
|
||||
|
||||
func StrFromPtr(ptr *string) string {
|
||||
if ptr == nil {
|
||||
|
|
|
@ -2,12 +2,15 @@ package util
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/nlopes/slack"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
var defaultChannel string
|
||||
|
@ -16,7 +19,18 @@ var slackApi *slack.Client
|
|||
|
||||
// InitSlack Initializes a slack client with the given token and sets the default channel.
|
||||
func InitSlack(token string, channel string, username string) {
|
||||
slackApi = slack.New(token)
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
slackApi = slack.New(token, slack.OptionHTTPClient(c))
|
||||
defaultChannel = channel
|
||||
defaultUsername = username
|
||||
}
|
||||
|
@ -65,7 +79,13 @@ func sendToSlack(channel, username, message string) error {
|
|||
err = errors.Err("no slack token provided")
|
||||
} else {
|
||||
log.Debugln("slack: " + channel + ": " + message)
|
||||
for {
|
||||
_, _, err = slackApi.PostMessage(channel, slack.MsgOptionText(message, false), slack.MsgOptionUsername(username))
|
||||
if err != nil && strings.Contains(err.Error(), "timeout awaiting response headers") {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
53
extras/util/strings.go
Normal file
53
extras/util/strings.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func StringSplitArg(stringToSplit, separator string) []interface{} {
|
||||
split := strings.Split(stringToSplit, separator)
|
||||
splitInterface := make([]interface{}, len(split))
|
||||
for i, s := range split {
|
||||
splitInterface[i] = s
|
||||
}
|
||||
return splitInterface
|
||||
}
|
||||
|
||||
// NormalizeName Normalize names to remove weird characters and account to capitalization
|
||||
func NormalizeName(s string) string {
|
||||
c := cases.Fold()
|
||||
return c.String(norm.NFD.String(s))
|
||||
}
|
||||
|
||||
// ReverseBytesInPlace reverse the bytes. thanks, Satoshi 😒
|
||||
func ReverseBytesInPlace(s []byte) {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
||||
|
||||
// TxIdToTxHash convert the txid to a hash for returning from the hub
|
||||
func TxIdToTxHash(txid string) []byte {
|
||||
t, err := hex.DecodeString(txid)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ReverseBytesInPlace(t)
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// TxHashToTxId convert the txHash from the response format back to an id
|
||||
func TxHashToTxId(txHash []byte) string {
|
||||
t := make([]byte, len(txHash))
|
||||
copy(t, txHash)
|
||||
|
||||
ReverseBytesInPlace(t)
|
||||
|
||||
return hex.EncodeToString(t)
|
||||
|
||||
}
|
90
go.mod
90
go.mod
|
@ -1,51 +1,55 @@
|
|||
module github.com/lbryio/lbry.go
|
||||
go 1.18
|
||||
|
||||
module github.com/lbryio/lbry.go/v2
|
||||
|
||||
replace github.com/btcsuite/btcd => github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf // indirect
|
||||
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32
|
||||
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/go-ini/ini v1.38.2
|
||||
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.3.0
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2
|
||||
github.com/gorilla/rpc v1.1.0
|
||||
github.com/gorilla/websocket v1.2.0 // indirect
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c
|
||||
github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002
|
||||
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04
|
||||
github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/go-ini/ini v1.67.0
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/rpc v1.2.0
|
||||
github.com/lbryio/ozzo-validation v3.0.3-0.20170512160344-202201e212ec+incompatible
|
||||
github.com/lbryio/types v0.0.0-20220224142228-73610f6654a6
|
||||
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675
|
||||
github.com/nlopes/slack v0.5.0
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930
|
||||
github.com/sirupsen/logrus v1.2.0
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
github.com/spf13/cast v1.2.0
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/uber-go/atomic v1.3.2
|
||||
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/sys v0.0.0-20190520201301-c432e742b0af // indirect
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c
|
||||
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
gopkg.in/ini.v1 v1.41.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/sebdah/goldie v1.0.0
|
||||
github.com/sergi/go-diff v1.3.1
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/slack-go/slack v0.12.1
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/ybbus/jsonrpc/v2 v2.1.7
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
golang.org/x/text v0.8.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
gotest.tools v2.2.0+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
230
go.sum
230
go.sum
|
@ -1,170 +1,150 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32 h1:qkOC5Gd33k54tobS36cXdAzJbeHaduLtnLQQwNoIi78=
|
||||
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803 h1:j3AgPKKZtZStM2nyhrDSLSYgT7YHrZKdSkq1OYeLjvM=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4=
|
||||
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible h1:sUy/in/P6askYr16XJgTKq/0SZhiWsdg4WZGaLsGQkM=
|
||||
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/rpc v1.1.0 h1:marKfvVP0Gpd/jHlVBKCQ8RAoUPdX7K1Nuh6l1BNh7A=
|
||||
github.com/gorilla/rpc v1.1.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk=
|
||||
github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89 h1:12K8AlpT0/6QUXSfV0yi4Q0jkbq8NDtIKFtF61AoqV0=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c h1:BhdcWGsuKif/XoSZnqVGNqJ1iEmH0czWR5upj+AuR8M=
|
||||
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8=
|
||||
github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002 h1:urfYK5ElpUrAv90auPLldoVC60LwiGAcY0OE6HJB9KI=
|
||||
github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo=
|
||||
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI=
|
||||
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4=
|
||||
github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c h1:m3O7561xBQ00lfUVayW4c6SnpVbUDQtPUwGcGYSUYQA=
|
||||
github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE=
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19 h1:/zWD8dVIl7bV1TdJWqPqy9tpqixzX2Qxgit48h3hQcY=
|
||||
github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/lbryio/ozzo-validation v3.0.3-0.20170512160344-202201e212ec+incompatible h1:OH/jgRO/2lQ73n7PgtK/CvLZ0dwAVr5G5s635+YfUA4=
|
||||
github.com/lbryio/ozzo-validation v3.0.3-0.20170512160344-202201e212ec+incompatible/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4=
|
||||
github.com/lbryio/types v0.0.0-20220224142228-73610f6654a6 h1:IhL9D2QfDWhLNDQpZ3Uiiw0gZEUYeLBS6uDqOd59G5o=
|
||||
github.com/lbryio/types v0.0.0-20220224142228-73610f6654a6/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE=
|
||||
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 h1:mG83tLXWSRdcXMWfkoumVwhcCbf3jHF9QKv/m37BkM0=
|
||||
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5/go.mod h1:H0aPCWffGOaDcjkw1iB7W9DVLp6GXmfcJY/7YZCWPA4=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 h1:/rdJjIiKG5rRdwG5yxHmSE/7ZREjpyC0kL7GxGT/qJw=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
|
||||
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 h1:IY+sDBJR/wRtsxq+626xJnt4Tw7/ROA9cDIR8MMhWyg=
|
||||
github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561/go.mod h1:lvjGftC8oe7XPtyrOidaMi0rp5B9+XY/ZRUynGnuaxQ=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930 h1:pSgp2x9zCkCjb8rxXFNpGE8hDIrt+UXW7jUQ5fbTlzM=
|
||||
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
|
||||
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
|
||||
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
|
||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d h1:tQo6hjclyv3RHUgZOl6iWb2Y44A/sN9bf9LAYfuioEg=
|
||||
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/ybbus/jsonrpc/v2 v2.1.7 h1:QjoXuZhkXZ3oLBkrONBe2avzFkYeYLorpeA+d8175XQ=
|
||||
github.com/ybbus/jsonrpc/v2 v2.1.7/go.mod h1:rIuG1+ORoiqocf9xs/v+ecaAVeo3zcZHQgInyKFMeg0=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190520201301-c432e742b0af h1:NXfmMfXz6JqGfG3ikSxcz2N93j6DgScr19Oo2uwFu88=
|
||||
golang.org/x/sys v0.0.0-20190520201301-c432e742b0af/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f h1:FU37niK8AQ59mHcskRyQL7H0ErSeNh650vdcj8HqdSI=
|
||||
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE=
|
||||
gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 h1:FpCr9V8wuOei4BAen+93HtVJ+XSi+KPbaPKm0Vj5R64=
|
||||
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79/go.mod h1:gWkaRU7CoXpezCBWfWjm3999QqS+1pYPXGbqQCTMzo8=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -3,7 +3,7 @@ package lbrycrd
|
|||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil"
|
||||
|
@ -34,7 +34,7 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (btcutil.Address, e
|
|||
if err == base58.ErrChecksum {
|
||||
return nil, btcutil.ErrChecksumMismatch
|
||||
}
|
||||
return nil, errors.Err("decoded address is of unknown format")
|
||||
return nil, errors.Err("decoded address[%s] is of unknown format even with default chainparams[%s]", addr, defaultNet.Name)
|
||||
}
|
||||
|
||||
switch len(decoded) {
|
||||
|
|
12
lbrycrd/address_test.go
Normal file
12
lbrycrd/address_test.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package lbrycrd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecodeAddress(t *testing.T) {
|
||||
addr := "bMUxfQVUeDi7ActVeZJZHzHKBceai7kHha"
|
||||
btcAddr, err := DecodeAddress(addr, &MainNetParams)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
println(btcAddr.EncodeAddress())
|
||||
}
|
|
@ -3,7 +3,7 @@ package lbrycrd_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/lbrycrd"
|
||||
"github.com/lbryio/lbry.go/v2/lbrycrd"
|
||||
)
|
||||
|
||||
var claimIdTests = []struct {
|
||||
|
|
40
lbrycrd/channel.go
Normal file
40
lbrycrd/channel.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package lbrycrd
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/keys"
|
||||
c "github.com/lbryio/lbry.go/v2/schema/stake"
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
)
|
||||
|
||||
func NewChannel() (*c.StakeHelper, *btcec.PrivateKey, error) {
|
||||
claimChannel := new(pb.Claim_Channel)
|
||||
channel := new(pb.Channel)
|
||||
claimChannel.Channel = channel
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = claimChannel
|
||||
|
||||
privateKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
return nil, nil, errors.Err(err)
|
||||
}
|
||||
pubkeyBytes, err := keys.PublicKeyToDER(privateKey.PubKey())
|
||||
if err != nil {
|
||||
return nil, nil, errors.Err(err)
|
||||
}
|
||||
|
||||
helper := c.StakeHelper{Claim: pbClaim}
|
||||
helper.Version = c.NoSig
|
||||
helper.Claim.GetChannel().PublicKey = pubkeyBytes
|
||||
helper.Claim.Tags = []string{}
|
||||
coverSrc := new(pb.Source)
|
||||
helper.Claim.GetChannel().Cover = coverSrc
|
||||
helper.Claim.Languages = []*pb.Language{}
|
||||
thumbnailSrc := new(pb.Source)
|
||||
helper.Claim.Thumbnail = thumbnailSrc
|
||||
helper.Claim.Locations = []*pb.Location{}
|
||||
|
||||
return &helper, privateKey, nil
|
||||
}
|
86
lbrycrd/claim.go
Normal file
86
lbrycrd/claim.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package lbrycrd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
c "github.com/lbryio/lbry.go/v2/schema/stake"
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
)
|
||||
|
||||
func NewImageStreamClaim() (*c.StakeHelper, error) {
|
||||
streamClaim := new(pb.Claim_Stream)
|
||||
stream := new(pb.Stream)
|
||||
image := new(pb.Stream_Image)
|
||||
image.Image = new(pb.Image)
|
||||
stream.Type = image
|
||||
|
||||
streamClaim.Stream = stream
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = streamClaim
|
||||
|
||||
helper := c.StakeHelper{Claim: pbClaim}
|
||||
|
||||
return &helper, nil
|
||||
}
|
||||
|
||||
func NewVideoStreamClaim() (*c.StakeHelper, error) {
|
||||
streamClaim := new(pb.Claim_Stream)
|
||||
stream := new(pb.Stream)
|
||||
video := new(pb.Stream_Video)
|
||||
video.Video = new(pb.Video)
|
||||
stream.Type = video
|
||||
streamClaim.Stream = stream
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = streamClaim
|
||||
|
||||
helper := c.StakeHelper{Claim: pbClaim}
|
||||
|
||||
return &helper, nil
|
||||
}
|
||||
|
||||
func NewStreamClaim(title, description string) (*c.StakeHelper, error) {
|
||||
streamClaim := new(pb.Claim_Stream)
|
||||
stream := new(pb.Stream)
|
||||
streamClaim.Stream = stream
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = streamClaim
|
||||
|
||||
helper := c.StakeHelper{Claim: pbClaim}
|
||||
helper.Claim.Title = title
|
||||
helper.Claim.Description = description
|
||||
|
||||
return &helper, nil
|
||||
}
|
||||
|
||||
func SignClaim(rawTx *wire.MsgTx, privKey btcec.PrivateKey, claim, channel *c.StakeHelper, channelClaimID string) error {
|
||||
claimIDHexBytes, err := hex.DecodeString(channelClaimID)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
claim.Version = c.WithSig
|
||||
claim.ClaimID = rev(claimIDHexBytes)
|
||||
hash, err := c.GetOutpointHash(rawTx.TxIn[0].PreviousOutPoint.Hash.String(), rawTx.TxIn[0].PreviousOutPoint.Index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig, err := c.Sign(privKey, *channel, *claim, hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lbrySig, err := sig.LBRYSDKEncode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
claim.Signature = lbrySig
|
||||
|
||||
return nil
|
||||
|
||||
}
|
|
@ -1,15 +1,20 @@
|
|||
package lbrycrd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
c "github.com/lbryio/lbry.go/v2/schema/stake"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/btcjson"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/go-ini/ini"
|
||||
)
|
||||
|
@ -28,10 +33,53 @@ var MainNetParams = chaincfg.Params{
|
|||
PubKeyHashAddrID: 0x55,
|
||||
ScriptHashAddrID: 0x7a,
|
||||
PrivateKeyID: 0x1c,
|
||||
Bech32HRPSegwit: "not-used", // we don't have this (yet)
|
||||
Bech32HRPSegwit: "lbc",
|
||||
//WitnessPubKeyHashAddrID: , // i cant find these in bitcoin codebase either
|
||||
//WitnessScriptHashAddrID:,
|
||||
GenesisHash: &GenesisHash,
|
||||
Name: "mainnet",
|
||||
Net: wire.BitcoinNet(0xfae4aaf1),
|
||||
DefaultPort: "9246",
|
||||
BIP0034Height: 1,
|
||||
BIP0065Height: 200000,
|
||||
BIP0066Height: 200000,
|
||||
}
|
||||
|
||||
const (
|
||||
lbrycrdMainPubkeyPrefix = byte(85)
|
||||
lbrycrdMainScriptPrefix = byte(122)
|
||||
lbrycrdTestnetPubkeyPrefix = byte(111)
|
||||
lbrycrdTestnetScriptPrefix = byte(196)
|
||||
lbrycrdRegtestPubkeyPrefix = byte(111)
|
||||
lbrycrdRegtestScriptPrefix = byte(196)
|
||||
|
||||
LbrycrdMain = "lbrycrd_main"
|
||||
LbrycrdTestnet = "lbrycrd_testnet"
|
||||
LbrycrdRegtest = "lbrycrd_regtest"
|
||||
)
|
||||
|
||||
var mainNetParams = chaincfg.Params{
|
||||
PubKeyHashAddrID: lbrycrdMainPubkeyPrefix,
|
||||
ScriptHashAddrID: lbrycrdMainScriptPrefix,
|
||||
PrivateKeyID: 0x1c,
|
||||
}
|
||||
|
||||
var testNetParams = chaincfg.Params{
|
||||
PubKeyHashAddrID: lbrycrdTestnetPubkeyPrefix,
|
||||
ScriptHashAddrID: lbrycrdTestnetScriptPrefix,
|
||||
PrivateKeyID: 0x1c,
|
||||
Bech32HRPSegwit: "tlbc",
|
||||
}
|
||||
|
||||
var regTestNetParams = chaincfg.Params{
|
||||
PubKeyHashAddrID: lbrycrdRegtestPubkeyPrefix,
|
||||
ScriptHashAddrID: lbrycrdRegtestScriptPrefix,
|
||||
PrivateKeyID: 0x1c,
|
||||
Bech32HRPSegwit: "rlbc",
|
||||
}
|
||||
|
||||
var ChainParamsMap = map[string]chaincfg.Params{LbrycrdMain: mainNetParams, LbrycrdTestnet: testNetParams, LbrycrdRegtest: regTestNetParams}
|
||||
|
||||
func init() {
|
||||
// Register lbrycrd network
|
||||
err := chaincfg.Register(&MainNetParams)
|
||||
|
@ -46,7 +94,7 @@ type Client struct {
|
|||
}
|
||||
|
||||
// New initializes a new Client
|
||||
func New(lbrycrdURL string) (*Client, error) {
|
||||
func New(lbrycrdURL string, chainParams *chaincfg.Params) (*Client, error) {
|
||||
// Connect to local bitcoin core RPC server using HTTP POST mode.
|
||||
|
||||
u, err := url.Parse(lbrycrdURL)
|
||||
|
@ -60,10 +108,16 @@ func New(lbrycrdURL string) (*Client, error) {
|
|||
|
||||
password, _ := u.User.Password()
|
||||
|
||||
chain := MainNetParams
|
||||
if chainParams != nil {
|
||||
chain = *chainParams
|
||||
}
|
||||
|
||||
connCfg := &rpcclient.ConnConfig{
|
||||
Host: u.Host,
|
||||
User: u.User.Username(),
|
||||
Pass: password,
|
||||
Params: chain,
|
||||
HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode
|
||||
DisableTLS: true, // Bitcoin core does not provide TLS by default
|
||||
}
|
||||
|
@ -74,7 +128,7 @@ func New(lbrycrdURL string) (*Client, error) {
|
|||
}
|
||||
|
||||
// make sure lbrycrd is running and responsive
|
||||
_, err = client.GetInfo()
|
||||
_, err = client.GetBlockChainInfo()
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
@ -82,12 +136,12 @@ func New(lbrycrdURL string) (*Client, error) {
|
|||
return &Client{client}, nil
|
||||
}
|
||||
|
||||
func NewWithDefaultURL() (*Client, error) {
|
||||
func NewWithDefaultURL(chainParams *chaincfg.Params) (*Client, error) {
|
||||
url, err := getLbrycrdURLFromConfFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(url)
|
||||
return New(url, chainParams)
|
||||
}
|
||||
|
||||
var errInsufficientFunds = errors.Base("insufficient funds")
|
||||
|
@ -104,7 +158,7 @@ func (c *Client) SimpleSend(toAddress string, amount float64) (*chainhash.Hash,
|
|||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
hash, err := c.Client.SendFromMinConf("", decodedAddress, lbcAmount, 0)
|
||||
hash, err := c.Client.SendToAddress(decodedAddress, lbcAmount)
|
||||
if err != nil {
|
||||
if err.Error() == "-6: Insufficient funds" {
|
||||
err = errors.Err(errInsufficientFunds)
|
||||
|
@ -114,30 +168,6 @@ func (c *Client) SimpleSend(toAddress string, amount float64) (*chainhash.Hash,
|
|||
return hash, nil
|
||||
}
|
||||
|
||||
//func (c *Client) SendWithSplit(toAddress string, amount float64, numUTXOs int) (*chainhash.Hash, error) {
|
||||
// decodedAddress, err := DecodeAddress(toAddress, &MainNetParams)
|
||||
// if err != nil {
|
||||
// return nil, errors.Wrap(err, 0)
|
||||
// }
|
||||
//
|
||||
// amountPerAddress, err := btcutil.NewAmount(amount / float64(numUTXOs))
|
||||
// if err != nil {
|
||||
// return nil, errors.Wrap(err, 0)
|
||||
// }
|
||||
//
|
||||
// amounts := map[btcutil.Address]btcutil.Amount{}
|
||||
// for i := 0; i < numUTXOs; i++ {
|
||||
// addr := decodedAddress // to give it a new address, so
|
||||
// amounts[addr] = amountPerAddress
|
||||
// }
|
||||
//
|
||||
// hash, err := c.Client.SendManyMinConf("", amounts, 0)
|
||||
// if err != nil && err.Error() == "-6: Insufficient funds" {
|
||||
// err = errors.Wrap(errInsufficientFunds, 0)
|
||||
// }
|
||||
// return hash, errors.Wrap(err, 0)
|
||||
//}
|
||||
|
||||
func getLbrycrdURLFromConfFile() (string, error) {
|
||||
if os.Getenv("HOME") == "" {
|
||||
return "", errors.Err("no $HOME var found")
|
||||
|
@ -179,3 +209,179 @@ func getLbrycrdURLFromConfFile() (string, error) {
|
|||
|
||||
return "rpc://" + userpass + host + ":" + port, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateBaseRawTx(inputs []btcjson.TransactionInput, change float64) (*wire.MsgTx, error) {
|
||||
addresses := make(map[btcutil.Address]btcutil.Amount)
|
||||
changeAddress, err := c.GetNewAddress("")
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
changeAmount, err := btcutil.NewAmount(change)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
addresses[changeAddress] = changeAmount
|
||||
lockTime := int64(0)
|
||||
return c.CreateRawTransaction(inputs, addresses, &lockTime)
|
||||
}
|
||||
|
||||
func (c *Client) GetEmptyTx(totalOutputSpend float64) (*wire.MsgTx, error) {
|
||||
totalFees := 0.1
|
||||
unspentResults, err := c.ListUnspentMin(1)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
finder := newOutputFinder(unspentResults)
|
||||
|
||||
outputs, err := finder.nextBatch(totalOutputSpend + totalFees)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(outputs) == 0 {
|
||||
return nil, errors.Err("Not enough spendable outputs to create transaction")
|
||||
}
|
||||
inputs := make([]btcjson.TransactionInput, len(outputs))
|
||||
var totalInputSpend float64
|
||||
for i, output := range outputs {
|
||||
inputs[i] = btcjson.TransactionInput{Txid: output.TxID, Vout: output.Vout}
|
||||
totalInputSpend = totalInputSpend + output.Amount
|
||||
}
|
||||
|
||||
change := totalInputSpend - totalOutputSpend - totalFees
|
||||
return c.CreateBaseRawTx(inputs, change)
|
||||
}
|
||||
|
||||
func (c *Client) SignTxAndSend(rawTx *wire.MsgTx) (*chainhash.Hash, error) {
|
||||
signedTx, allInputsSigned, err := c.SignRawTransaction(rawTx)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
if !allInputsSigned {
|
||||
return nil, errors.Err("Not all inputs for the tx could be signed!")
|
||||
}
|
||||
|
||||
return c.SendRawTransaction(signedTx, false)
|
||||
}
|
||||
|
||||
type ScriptType int
|
||||
|
||||
const (
|
||||
ClaimName ScriptType = iota
|
||||
ClaimUpdate
|
||||
ClaimSupport
|
||||
)
|
||||
|
||||
func (c *Client) AddStakeToTx(rawTx *wire.MsgTx, claim *c.StakeHelper, name string, claimAmount float64, scriptType ScriptType) error {
|
||||
|
||||
address, err := c.GetNewAddress("")
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
amount, err := btcutil.NewAmount(claimAmount)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
|
||||
value, err := claim.CompileValue()
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
var claimID string
|
||||
if len(claim.ClaimID) > 0 {
|
||||
claimID = hex.EncodeToString(rev(claim.ClaimID))
|
||||
}
|
||||
var script []byte
|
||||
switch scriptType {
|
||||
case ClaimName:
|
||||
script, err = getClaimNamePayoutScript(name, value, address)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
case ClaimUpdate:
|
||||
script, err = getUpdateClaimPayoutScript(name, claimID, value, address)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
case ClaimSupport:
|
||||
script, err = getUpdateClaimPayoutScript(name, claimID, value, address)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
rawTx.AddTxOut(wire.NewTxOut(int64(amount), script))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateChannel(name string, amount float64) (*c.StakeHelper, *btcec.PrivateKey, error) {
|
||||
channel, key, err := NewChannel()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rawTx, err := c.GetEmptyTx(amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = c.AddStakeToTx(rawTx, channel, name, amount, ClaimName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err = c.SignTxAndSend(rawTx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return channel, key, nil
|
||||
}
|
||||
|
||||
func (c *Client) SupportClaim(name, claimID, address, blockchainName string, claimAmount float64) (*chainhash.Hash, error) {
|
||||
const DefaultFeePerSupport = float64(0.0001)
|
||||
unspentResults, err := c.ListUnspentMin(1)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
finder := newOutputFinder(unspentResults)
|
||||
outputs, err := finder.nextBatch(claimAmount + DefaultFeePerSupport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(outputs) == 0 {
|
||||
return nil, errors.Err("Not enough spendable outputs to create transaction")
|
||||
}
|
||||
inputs := make([]btcjson.TransactionInput, len(outputs))
|
||||
|
||||
var totalInputSpend float64
|
||||
for i, output := range outputs {
|
||||
inputs[i] = btcjson.TransactionInput{Txid: output.TxID, Vout: output.Vout}
|
||||
totalInputSpend = totalInputSpend + output.Amount
|
||||
}
|
||||
|
||||
change := totalInputSpend - claimAmount - DefaultFeePerSupport
|
||||
rawTx, err := c.CreateBaseRawTx(inputs, change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chainParams, ok := ChainParamsMap[blockchainName]
|
||||
if !ok {
|
||||
return nil, errors.Err("invalid blockchain name %s", blockchainName)
|
||||
}
|
||||
decodedAddress, err := DecodeAddress(address, &chainParams)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
amount, err := btcutil.NewAmount(claimAmount)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
script, err := getClaimSupportPayoutScript(name, claimID, decodedAddress)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
rawTx.AddTxOut(wire.NewTxOut(int64(amount), script))
|
||||
|
||||
return c.SignTxAndSend(rawTx)
|
||||
}
|
||||
|
|
38
lbrycrd/finder.go
Normal file
38
lbrycrd/finder.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package lbrycrd
|
||||
|
||||
import (
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/btcsuite/btcd/btcjson"
|
||||
)
|
||||
|
||||
type outputFinder struct {
|
||||
unspent []btcjson.ListUnspentResult
|
||||
lastChecked int
|
||||
}
|
||||
|
||||
func newOutputFinder(unspentResults []btcjson.ListUnspentResult) *outputFinder {
|
||||
return &outputFinder{unspent: unspentResults, lastChecked: -1}
|
||||
}
|
||||
|
||||
func (f *outputFinder) nextBatch(minAmount float64) ([]btcjson.ListUnspentResult, error) {
|
||||
var batch []btcjson.ListUnspentResult
|
||||
var lbcBatched float64
|
||||
for i, unspent := range f.unspent {
|
||||
if i > f.lastChecked {
|
||||
if unspent.Spendable {
|
||||
batch = append(batch, unspent)
|
||||
lbcBatched = lbcBatched + unspent.Amount
|
||||
}
|
||||
}
|
||||
if lbcBatched >= minAmount {
|
||||
f.lastChecked = i
|
||||
break
|
||||
}
|
||||
if i == len(f.unspent)-1 {
|
||||
return nil, errors.Err("Not enough unspent outputs to spend %d on supports.", minAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return batch, nil
|
||||
}
|
76
lbrycrd/script.go
Normal file
76
lbrycrd/script.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package lbrycrd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
func getClaimSupportPayoutScript(name, claimid string, address btcutil.Address) ([]byte, error) {
|
||||
//OP_SUPPORT_CLAIM <name> <claimid> OP_2DROP OP_DROP OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
bytes, err := hex.DecodeString(claimid)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
return txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_NOP7). //OP_SUPPORT_CLAIM
|
||||
AddData([]byte(name)). //<name>
|
||||
AddData(rev(bytes)). //<claimid>
|
||||
AddOp(txscript.OP_2DROP). //OP_2DROP
|
||||
AddOp(txscript.OP_DROP). //OP_DROP
|
||||
AddOps(pkscript). //OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
Script()
|
||||
|
||||
}
|
||||
|
||||
func getClaimNamePayoutScript(name string, value []byte, address btcutil.Address) ([]byte, error) {
|
||||
//OP_CLAIM_NAME <name> <value> OP_2DROP OP_DROP OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
return txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_NOP6). //OP_CLAIMNAME
|
||||
AddData([]byte(name)). //<name>
|
||||
AddData(value). //<value>
|
||||
AddOp(txscript.OP_2DROP). //OP_2DROP
|
||||
AddOp(txscript.OP_DROP). //OP_DROP
|
||||
AddOps(pkscript). //OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
Script()
|
||||
}
|
||||
|
||||
func getUpdateClaimPayoutScript(name, claimid string, value []byte, address btcutil.Address) ([]byte, error) {
|
||||
//OP_UPDATE_CLAIM <name> <claimid> <value> OP_2DROP OP_DROP OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
bytes, err := hex.DecodeString(claimid)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
return txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_NOP8). //OP_UPDATE_CLAIM
|
||||
AddData([]byte(name)). //<name>
|
||||
AddData(rev(bytes)). //<claimid>
|
||||
AddData(value). //<value>
|
||||
AddOp(txscript.OP_2DROP). //OP_2DROP
|
||||
AddOp(txscript.OP_DROP). //OP_DROP
|
||||
AddOps(pkscript). //OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG
|
||||
Script()
|
||||
}
|
2
main.go
2
main.go
|
@ -6,7 +6,7 @@ import (
|
|||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbry.go/claim"
|
||||
"github.com/lbryio/lbry.go/v2/claim"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
lbry.go is a set of tools and projects implemented in Golang. See each subfolder for more details
|
||||
|
||||
**there are significant updates in the [v3 branch](https://github.com/lbryio/lbry.go/tree/v3). if you're starting a new project, strongly consider using that version instead**
|
||||
|
||||
[![Build Status](https://travis-ci.org/lbryio/lbry.go.svg?branch=master)](https://travis-ci.org/lbryio/lbry.go)
|
||||
|
||||
This project uses Go modules. Make sure you have Go 1.11+ installed.
|
||||
|
@ -34,7 +36,7 @@ This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
|||
## Security
|
||||
|
||||
We take security seriously. Please contact security@lbry.com regarding any issues you may encounter.
|
||||
Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
|
||||
Our PGP key is [here](https://lbry.com/faq/pgp-key) if you need it.
|
||||
|
||||
## Contact
|
||||
|
||||
|
|
4
schema/.gitignore
vendored
Normal file
4
schema/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.idea/
|
||||
lbryschema-cli
|
||||
lbryschema-python-binding.h
|
||||
lbryschema-python-binding.so
|
9
schema/.travis.yml
Normal file
9
schema/.travis.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
os: linux
|
||||
dist: trusty
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.15.x
|
||||
|
||||
script:
|
||||
- ./build_and_test.sh
|
29
schema/address/address_test.go
Normal file
29
schema/address/address_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package address
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecodeAddressLBRYCrdMain(t *testing.T) {
|
||||
addr := "bUc9gyCJPKu2CBYpTvJ98MdmsLb68utjP6"
|
||||
correct := [25]byte{85, 174, 41, 64, 245, 110, 91, 239, 43, 208, 32, 73, 115, 20, 70, 204, 83, 199, 3,
|
||||
206, 210, 176, 194, 188, 193}
|
||||
result, err := DecodeAddress(addr, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != correct {
|
||||
t.Error("Mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAddressLBRYCrdMain(t *testing.T) {
|
||||
addr := [25]byte{85, 174, 41, 64, 245, 110, 91, 239, 43, 208, 32, 73, 115, 20, 70, 204, 83, 199, 3,
|
||||
206, 210, 176, 194, 188, 193}
|
||||
correct := "bUc9gyCJPKu2CBYpTvJ98MdmsLb68utjP6"
|
||||
result, err := EncodeAddress(addr, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != correct {
|
||||
t.Error("Mismatch")
|
||||
}
|
||||
}
|
1
schema/address/base58/base58_test.go
Normal file
1
schema/address/base58/base58_test.go
Normal file
|
@ -0,0 +1 @@
|
|||
package base58
|
21
schema/address/base58/character.go
Normal file
21
schema/address/base58/character.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package base58
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var b58Characters = [58]byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45,
|
||||
0x46, 0x47, 0x48, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55,
|
||||
0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||
0x6a, 0x6b, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7a}
|
||||
|
||||
func CharacterIndex(character byte) (*big.Int, error) {
|
||||
for i := 0; i < 58; i++ {
|
||||
if b58Characters[i] == character {
|
||||
return big.NewInt(int64(i)), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid character")
|
||||
}
|
20
schema/address/base58/checksum.go
Normal file
20
schema/address/base58/checksum.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package base58
|
||||
|
||||
import "crypto/sha256"
|
||||
|
||||
const checksumLength = 4
|
||||
|
||||
func VerifyBase58Checksum(v []byte) bool {
|
||||
checksum := [checksumLength]byte{}
|
||||
for i := range checksum {
|
||||
checksum[i] = v[len(v)-checksumLength+i]
|
||||
}
|
||||
real_checksum := sha256.Sum256(v[:len(v)-checksumLength])
|
||||
real_checksum = sha256.Sum256(real_checksum[:])
|
||||
for i, c := range checksum {
|
||||
if c != real_checksum[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
39
schema/address/base58/decode.go
Normal file
39
schema/address/base58/decode.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package base58
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func DecodeBase58(value string, size int64) ([]byte, error) {
|
||||
buf := []byte(value)
|
||||
longValue := big.NewInt(0)
|
||||
result := make([]byte, size)
|
||||
for i := int64(len(buf) - 1); i >= 0; i-- {
|
||||
to_add := big.NewInt(0)
|
||||
to_add = to_add.Exp(big.NewInt(58), big.NewInt(i), to_add)
|
||||
c, err := CharacterIndex(buf[int64(len(buf))-i-1])
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
to_add = to_add.Mul(c, to_add)
|
||||
longValue = longValue.Add(to_add, longValue)
|
||||
}
|
||||
for i := size - 1; i >= 0; i-- {
|
||||
m := big.NewInt(0)
|
||||
longValue, m = longValue.DivMod(longValue, big.NewInt(256), m)
|
||||
bs := m.Bytes()
|
||||
if len(bs) == 0 {
|
||||
bs = append(bs, 0x00)
|
||||
}
|
||||
b := byte(bs[0])
|
||||
result[i] = b
|
||||
}
|
||||
if longValue.Int64() != 0 {
|
||||
return result, errors.New("cannot decode to the given size")
|
||||
}
|
||||
if size != int64(len(result)) {
|
||||
return result, errors.New("length mismatch")
|
||||
}
|
||||
return result, nil
|
||||
}
|
32
schema/address/base58/encode.go
Normal file
32
schema/address/base58/encode.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package base58
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func EncodeBase58(data []byte) string {
|
||||
longValue := big.NewInt(0)
|
||||
result := ""
|
||||
for i := 0; i < len(data); i++ {
|
||||
to_add := big.NewInt(0)
|
||||
to_add = to_add.Exp(big.NewInt(256), big.NewInt(int64(i)), to_add)
|
||||
to_add = to_add.Mul(big.NewInt(int64(data[24-i])), to_add)
|
||||
longValue = longValue.Add(to_add, longValue)
|
||||
}
|
||||
i := 0
|
||||
for {
|
||||
m := big.NewInt(0)
|
||||
longValue, m = longValue.DivMod(longValue, big.NewInt(58), m)
|
||||
bs := m.Bytes()
|
||||
if len(bs) == 0 {
|
||||
bs = append(bs, 0x00)
|
||||
}
|
||||
b := b58Characters[bs[0]]
|
||||
result = string(b) + result
|
||||
if longValue.Int64() == 0 {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return result
|
||||
}
|
19
schema/address/decode.go
Normal file
19
schema/address/decode.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package address
|
||||
|
||||
import (
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/address/base58"
|
||||
)
|
||||
|
||||
func DecodeAddress(address string, blockchainName string) ([addressLength]byte, error) {
|
||||
decoded, err := base58.DecodeBase58(address, addressLength)
|
||||
if err != nil {
|
||||
return [addressLength]byte{}, errors.Err("failed to decode")
|
||||
}
|
||||
buf := [addressLength]byte{}
|
||||
for i, b := range decoded {
|
||||
buf[i] = b
|
||||
}
|
||||
|
||||
return ValidateAddress(buf, blockchainName)
|
||||
}
|
13
schema/address/encode.go
Normal file
13
schema/address/encode.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package address
|
||||
|
||||
import (
|
||||
"github.com/lbryio/lbry.go/v2/schema/address/base58"
|
||||
)
|
||||
|
||||
func EncodeAddress(address [addressLength]byte, blockchainName string) (string, error) {
|
||||
buf, err := ValidateAddress(address, blockchainName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base58.EncodeBase58(buf[:]), nil
|
||||
}
|
69
schema/address/validate.go
Normal file
69
schema/address/validate.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package address
|
||||
|
||||
import (
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/address/base58"
|
||||
)
|
||||
|
||||
const lbrycrdMainPubkeyPrefix = byte(85)
|
||||
const lbrycrdMainScriptPrefix = byte(122)
|
||||
|
||||
const lbrycrdTestnetPubkeyPrefix = byte(111)
|
||||
const lbrycrdTestnetScriptPrefix = byte(196)
|
||||
|
||||
const lbrycrdRegtestPubkeyPrefix = byte(111)
|
||||
const lbrycrdRegtestScriptPrefix = byte(196)
|
||||
|
||||
const prefixLength = 1
|
||||
const pubkeyLength = 20
|
||||
const checksumLength = 4
|
||||
const addressLength = prefixLength + pubkeyLength + checksumLength
|
||||
|
||||
const lbrycrdMain = "lbrycrd_main"
|
||||
const lbrycrdTestnet = "lbrycrd_testnet"
|
||||
const lbrycrdRegtest = "lbrycrd_regtest"
|
||||
|
||||
var addressPrefixes = map[string][2]byte{
|
||||
lbrycrdMain: {lbrycrdMainPubkeyPrefix, lbrycrdMainScriptPrefix},
|
||||
lbrycrdTestnet: {lbrycrdTestnetPubkeyPrefix, lbrycrdTestnetScriptPrefix},
|
||||
lbrycrdRegtest: {lbrycrdRegtestPubkeyPrefix, lbrycrdRegtestScriptPrefix},
|
||||
}
|
||||
|
||||
func PrefixIsValid(address [addressLength]byte, blockchainName string) bool {
|
||||
prefix := address[0]
|
||||
for _, addrPrefix := range addressPrefixes[blockchainName] {
|
||||
if addrPrefix == prefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PubKeyIsValid(address [addressLength]byte) bool {
|
||||
pubkey := address[prefixLength : pubkeyLength+prefixLength]
|
||||
// TODO: validate this for real
|
||||
if len(pubkey) != pubkeyLength {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ChecksumIsValid(address [addressLength]byte) bool {
|
||||
return base58.VerifyBase58Checksum(address[:])
|
||||
}
|
||||
|
||||
func ValidateAddress(address [addressLength]byte, blockchainName string) ([addressLength]byte, error) {
|
||||
if blockchainName != lbrycrdMain && blockchainName != lbrycrdTestnet && blockchainName != lbrycrdRegtest {
|
||||
return address, errors.Err("invalid blockchain name")
|
||||
}
|
||||
if !PrefixIsValid(address, blockchainName) {
|
||||
return address, errors.Err("invalid prefix")
|
||||
}
|
||||
if !PubKeyIsValid(address) {
|
||||
return address, errors.Err("invalid pubkey")
|
||||
}
|
||||
if !ChecksumIsValid(address) {
|
||||
return address, errors.Err("invalid address checksum")
|
||||
}
|
||||
return address, nil
|
||||
}
|
5
schema/build.sh
Executable file
5
schema/build.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
go build ./...
|
||||
go build ./cli/lbryschema-cli.go
|
5
schema/build_and_test.sh
Executable file
5
schema/build_and_test.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
./build.sh
|
||||
./test.sh
|
44
schema/cli/lbryschema-cli.go
Normal file
44
schema/cli/lbryschema-cli.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/schema/stake"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
if len(args) == 1 {
|
||||
claimBytes := []byte(args[0])
|
||||
decoded, err := stake.DecodeClaimBytes(claimBytes, "lbrycrd_main")
|
||||
if err != nil {
|
||||
fmt.Println("Decoding error:", err)
|
||||
return
|
||||
}
|
||||
text, err := decoded.RenderJSON()
|
||||
if err != nil {
|
||||
fmt.Println("Decoding error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(text)
|
||||
return
|
||||
} else if (len(args) == 2) && (args[1] == "--decode_hex") {
|
||||
claimHex := args[0]
|
||||
decoded, err := stake.DecodeClaimHex(claimHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
fmt.Println("Decoding error:", err)
|
||||
return
|
||||
}
|
||||
text, err := decoded.RenderJSON()
|
||||
if err != nil {
|
||||
fmt.Println("Decoding error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(text)
|
||||
return
|
||||
} else {
|
||||
fmt.Println("encountered an error\nusage: \n\tlbryschema-cli <value to decode> [--decode_hex]")
|
||||
return
|
||||
}
|
||||
}
|
108
schema/keys/keys.go
Normal file
108
schema/keys/keys.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
)
|
||||
|
||||
type publicKeyInfo struct {
|
||||
Raw asn1.RawContent
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
PublicKey asn1.BitString
|
||||
}
|
||||
|
||||
func PublicKeyToDER(publicKey *btcec.PublicKey) ([]byte, error) {
|
||||
var publicKeyBytes []byte
|
||||
var publicKeyAlgorithm pkix.AlgorithmIdentifier
|
||||
var err error
|
||||
pub := publicKey.ToECDSA()
|
||||
publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y)
|
||||
//ans1 encoding oid for ecdsa public key https://github.com/golang/go/blob/release-branch.go1.12/src/crypto/x509/x509.go#L457
|
||||
publicKeyAlgorithm.Algorithm = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
||||
//asn1 encoding oid for secp256k1 https://github.com/bitpay/bitpay-go/blob/v2.2.2/key_utils/key_utils.go#L30
|
||||
paramBytes, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 132, 0, 10})
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
publicKeyAlgorithm.Parameters.FullBytes = paramBytes
|
||||
|
||||
return asn1.Marshal(publicKeyInfo{
|
||||
Algorithm: publicKeyAlgorithm,
|
||||
PublicKey: asn1.BitString{
|
||||
Bytes: publicKeyBytes,
|
||||
BitLength: 8 * len(publicKeyBytes),
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//This type provides compatibility with the btcec package
|
||||
type ecPrivateKey struct {
|
||||
Version int
|
||||
PrivateKey []byte
|
||||
NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"`
|
||||
PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"`
|
||||
}
|
||||
|
||||
func PrivateKeyToDER(key *btcec.PrivateKey) ([]byte, error) {
|
||||
privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8)
|
||||
oid := asn1.ObjectIdentifier{1, 3, 132, 0, 10}
|
||||
return asn1.Marshal(ecPrivateKey{
|
||||
Version: 1,
|
||||
PrivateKey: key.D.FillBytes(privateKey),
|
||||
NamedCurveOID: oid,
|
||||
PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)},
|
||||
})
|
||||
}
|
||||
|
||||
func GetPublicKeyFromBytes(pubKeyBytes []byte) (*btcec.PublicKey, error) {
|
||||
if len(pubKeyBytes) == 33 {
|
||||
return btcec.ParsePubKey(pubKeyBytes, btcec.S256())
|
||||
}
|
||||
PKInfo := publicKeyInfo{}
|
||||
_, err := asn1.Unmarshal(pubKeyBytes, &PKInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
pubkeyBytes1 := PKInfo.PublicKey.Bytes
|
||||
return btcec.ParsePubKey(pubkeyBytes1, btcec.S256())
|
||||
}
|
||||
|
||||
func GetPrivateKeyFromBytes(privKeyBytes []byte) (*btcec.PrivateKey, *btcec.PublicKey, error) {
|
||||
ecPK := ecPrivateKey{}
|
||||
_, err := asn1.Unmarshal(privKeyBytes, &ecPK)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Err(err)
|
||||
}
|
||||
priv, publ := btcec.PrivKeyFromBytes(btcec.S256(), ecPK.PrivateKey)
|
||||
return priv, publ, nil
|
||||
}
|
||||
|
||||
//Returns a btec.Private key object if provided a correct secp256k1 encoded pem.
|
||||
func ExtractKeyFromPem(pm string) (*btcec.PrivateKey, *btcec.PublicKey) {
|
||||
byta := []byte(pm)
|
||||
blck, _ := pem.Decode(byta)
|
||||
var ecp ecPrivateKey
|
||||
asn1.Unmarshal(blck.Bytes, &ecp)
|
||||
return btcec.PrivKeyFromBytes(btcec.S256(), ecp.PrivateKey)
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
btcec.Signature
|
||||
}
|
||||
|
||||
func (s *Signature) LBRYSDKEncode() ([]byte, error) {
|
||||
if s.R == nil || s.S == nil {
|
||||
return nil, errors.Err("invalid signature, both S & R are nil")
|
||||
}
|
||||
rBytes := s.R.Bytes()
|
||||
sBytes := s.S.Bytes()
|
||||
|
||||
return append(rBytes, sBytes...), nil
|
||||
}
|
95
schema/keys/keys_test.go
Normal file
95
schema/keys/keys_test.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
// The purpose of this test, is to make sure the function converts btcec.PublicKey to DER format the same way
|
||||
// lbry SDK does as this is the bytes that are put into protobuf and the same bytes are used for verify signatures.
|
||||
// Making sure these
|
||||
func TestPublicKeyToDER(t *testing.T) {
|
||||
publicKeyHex := "3056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367"
|
||||
pubKeyBytes, err := hex.DecodeString(publicKeyHex)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
p1, err := GetPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
pubkeyBytes2, err := PublicKeyToDER(p1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for i, b := range pubKeyBytes {
|
||||
assert.Assert(t, b == pubkeyBytes2[i], "DER format in bytes must match!")
|
||||
}
|
||||
|
||||
p2, err := GetPublicKeyFromBytes(pubkeyBytes2)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, p1.IsEqual(p2), "The keys produced must be the same key!")
|
||||
}
|
||||
|
||||
func TestPrivateKeyToDER(t *testing.T) {
|
||||
private1, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := PrivateKeyToDER(private1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
private2, _, err := GetPrivateKeyFromBytes(bytes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !private1.ToECDSA().Equal(private2.ToECDSA()) {
|
||||
t.Error("private keys dont match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPrivateKeyFromBytes(t *testing.T) {
|
||||
private, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := PrivateKeyToDER(private)
|
||||
private2, _, err := GetPrivateKeyFromBytes(bytes)
|
||||
if !private.ToECDSA().Equal(private2.ToECDSA()) {
|
||||
t.Error("private keys dont match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodePEMAndBack(t *testing.T) {
|
||||
private1, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
derBytes, err := PrivateKeyToDER(private1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = pem.Encode(b, &pem.Block{Type: "PRIVATE KEY", Bytes: derBytes})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
println(string(b.Bytes()))
|
||||
private2, _ := ExtractKeyFromPem(string(b.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !private1.ToECDSA().Equal(private2.ToECDSA()) {
|
||||
t.Error("private keys dont match")
|
||||
}
|
||||
}
|
105
schema/stake/decode_test.go
Normal file
105
schema/stake/decode_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/schema/keys"
|
||||
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
)
|
||||
|
||||
type rawClaim struct {
|
||||
Hex string
|
||||
ClaimID string
|
||||
}
|
||||
|
||||
var raw_claims = []string{
|
||||
"08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367",
|
||||
"080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65",
|
||||
"080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f676966",
|
||||
"080110011af901080112b101080410011a1c43414e47474948207c204b412044494c554152204e4547455249207c222c0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d5f5470313577746e7753732a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f5f5470313577746e77537352005a001a41080110011a304616bfdbb6fcb870d4c235443f9261d289ee8edbd4a51b8c6e3e95a34baeebbbb08978a7c5f9bf9a36245513b450943b2209766964656f2f6d70342a5c080110031a40f94a9db9c70217e4f17f9d38f08770096e7ce94a86b742b972e07c62c9606459c6ad735cd517175cf76ad6ea9eb16ca8198a17e2d31dc3ac53413005b5ca2a3a221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
"080110011aa510080112dd0f080410011a4b4953545249204d4142554b204e41494b204b412021207b547269707d205369646f61726a6f202d2042616e797577616e67692042617275204e61696b204b41205372692054616e6a756e6722a80e4b657265746120617069205372692054616e6a756e67206164616c61682072616e676b6169616e206b657265746120617069206b656c617320656b6f6e6f6d69204143206a6172616b206a617568206d696c696b205054204b65726574612041706920496e646f6e6573696120285065727365726f292079616e67206d656c6179616e6920727574652042616e797577616e676920426172752d4c656d707579616e67616e2c2070702e204b65726574612061706920696e692064696f7065726173696b616e206f6c656820446165726168204f706572617369204958204a656d6265722c2079616e67206469616d62696c2064617269205372692054616e6a756e672c206e616d6120746f6b6f682064616c616d206365726974612072616b7961742042616e797577616e67692e0a0a4b65726574612061706920696e6920626572616e676b617420646172692042616e797577616e67692070756b756c2030362e3330205749422074696261206469204c656d707579616e67616e2070756b756c2031392e3330205749422c20736564616e676b616e20626572616e676b61742064617269204c656d707579616e67616e2070756b756c2030372e31352057494220746962612064692042616e797577616e67692070756b756c2032312e3135205749422e204b65726574612061706920696e69206d656d62617761207361747520676572626f6e6720616c696e672d616c696e6720626572757061206b65726574612070656d62616e676b6974202862696173616e7961204b5033292c20656e616d206b65726574612070656e756d70616e67206b656c617320656b6f6e6f6d692c2073617475206b6572657461206d616b616e2070656d62616e676b6974206b656c617320656b6f6e6f6d692c2064616e2068616d7069722073656c616c75206d656d626177612073617475206b65726574612062616761736920756e696b2079616e67206265727761726e61206269727520706f6c6f732e204e616d756e2073656972696e672064656e67616e2070656d62617275616e20696d616765205054204b41492c207365636172612062657274616861702073656d756120676572626f6e67206d656e6767756e616b616e206c697665727920224b65736570616b6174616e22206d656e67696b757469206b657265746120617069206c61696e6e79612e0a0a44616c616d207065726a616c616e616e6e79612c206b65726574612061706920696e692062657268656e7469206469205374617369756e2042616e797577616e676920426172752c204b6172616e676173656d2c20526f676f6a616d70692c2054656d7567757275682c204b616c6973657461696c2c2053756d626572776164756e672c20476c656e6d6f72652c204b616c69626172752c204b616c697361742c204a656d6265722c2052616d626970756a692c2054616e6767756c2c2050726f626f6c696e67676f2c20506173757275616e2c2042616e67696c2c205369646f61726a6f2c20537572616261796120477562656e672c20576f6e6f6b726f6d6f2c204d6f6a6f6b6572746f2c204a6f6d62616e672c204b6572746f736f6e6f2c204e67616e6a756b2c204361727562616e2c204d616469756e2c2042617261742c205061726f6e2c2057616c696b756b756e2c2053726167656e2c20507572776f736172692c204b6c6174656e2c2064616e204c656d707579616e67616e2c2064656e67616e20746f74616c2077616b74752074656d7075682073656b697461722031332d3134206a616d2e0a0a4b687573757320756e74756b206b652061726168205374617369756e2042616e797577616e6769204261727520284b41203139342f313935292c206b657265746120696e692064617061742062657268656e7469206469205374617369756e205361726164616e2028756e74756b2070657273696c616e67616e292c204261726f6e2c2053756d6f6269746f2028756e74756b2070657273696c616e67616e292c2064616e20536570616e6a616e672e204469205374617369756e20537572616261796120477562656e672c2064696c616b756b616e2070656d696e646168616e20706f73697369206c6f6b6f6d6f7469662e0a0a5061646120746168756e2032303136207461726966204b41205372692054616e6a756e67206b656d62616c69206469737562736964692070656d6572696e7461682e20506164612074616e6767616c2031204a616e756172692068696e676761203331204d617265742074617269666e7961206164616c6168205270203130302e3030302c30302c206d756c6169203120417072696c2074617269666e7961206164616c61682052702039362e3030302c30302c2064616e206d756c61692031204a756c692074617269666e7961206164616c61682052702039342e3030302c30302e202857696b697065646961290a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d754750344b5857614536512a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f754750344b58576145365152005a001a41080110011a30d3d1d49ce3268e3dcf318ebbb6f4bfd454995d6b772bd5e27630743c0fb1d66387bf84b51afe28733812c5495b837b8f2209766964656f2f6d70342a5c080110031a40a47aa2d45ec15d1e578b91e5c8c76ee8a82e55af37da4873a7703795889ee7400967cf41e903788bcf0510d7c06976c99983fa01e702e1fb6d518b0646b0d565221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
"080110011aa30d080112db0c080410011a3b5354415349554e2042414e595557414e47492042415255207c2050656e6767616e7469205374617369756e2042616e797577616e6769204c616d6122b60b5374617369756e2042616e797577616e676920426172752028425729206164616c6168207374617369756e206b657265746120617069206b656c61732062657361722079616e6720626572616461206469204b65746170616e672c204b616c697075726f2c2042616e797577616e67692e205374617369756e2079616e67207465726c6574616b2070616461206b6574696e676769616e202b37206d20696e69206d65727570616b616e207374617369756e2079616e67206c6574616b6e79612070616c696e672074696d757220646920446165726168204f706572617369204958204a656d6265722e205374617369756e20696e692062657261646120646920756a756e672070616c696e672074696d75722050756c6175204a6177612064616e2068616e7961206265726a6172616b20313030206d6574657220646172692050656c61627568616e2046657269204b65746170616e6720736568696e676761207374617369756e20696e69206a75676120736572696e672064697365627574205374617369756e204b65746170616e672e205374617369756e20696e69206a756761206d65727570616b616e207374617369756e206b65726574612061706920616b7469662079616e67206265726c6f6b6173692070616c696e672074696d75722064692042616e797577616e67692c204a6177612054696d75722c2064616e20496e646f6e657369612e205374617369756e20696e6920646962616e67756e2062657273616d61616e2064656e67616e2070656d62616e67756e616e206a616c757220626172752064617269207374617369756e206e6f6e20616b746966204b61626174206d656e756a752070656c61627568616e207465727365627574207061646120746168756e20313938353b20646966756e6773696b616e20756e74756b206d656e6767616e74696b616e205374617369756e2042616e797577616e6769204c616d612079616e67206164612064692077696c61796168206b6f74612042616e797577616e67692e205374617369756e2042616e797577616e67692042617275207465726c6574616b203130206b6d20646172692077696c61796168206b6f7461206b6520617261682075746172613b20646962616e67756e20756e74756b206d656d656e756869206b656275747568616e207472616e73706f72746173692079616e672073696e657267697320616e74617261206b6572657461206170692064656e67616e206b6170616c20666572692064692070656e7965626572616e67616e204b65746170616e672e205374617369756e20696e69206d656d696c696b6920656e616d206a616c75722064656e67616e206a616c757220322073656261676169207365707572206c757275732e0a5374617369756e20696e692064696c656e676b6170692064656e67616e20737562206469706f206c6f6b6f6d6f7469662064616e206469706f206b65726574612020756e74756b206d656e79696d70616e2c206d6572617761742061726d616461206b657265746120617069206b68757375736e7961206d696c696b2044616f70204958206974752073656e646972692c206a756761206d656d70756e79616920207475726e7461626c652042616c6c6f6f6e204c6f6f702079616e67207465726c6574616b20646920736562656c61682075746172612e0a5374617369756e20696e69206a756761206d656c6179616e6920616e676b7574616e20626172616e672c207961697475206b6572657461206170692053656d656e205469676120526f64612079616e67206469626572616e676b61746b616e2064617269205374617369756e204e616d626f2064616e206d656e6a616469204b412079616e67206d656e656d707568206a6172616b2070616c696e67206a61756820646920496e646f6e657369612e5b77696b6970656469615d0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d475473535a5a30794a53452a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f475473535a5a30794a534552005a001a41080110011a3048b3efe92661810e11118c9f8c0b4b4d1bca195eb6f74c8325070d97c699f4fb7ecc9ac90b3decc4feeb2ea0431e65922209766964656f2f6d70342a5c080110031a408e9fc836cad00c52ec7cdc95c11fc5369874948891df2187faaee212ca0925fc1058df5339c153dc00f055a8a21b853fb449a8ccb25ea52a98ba5645b22bdbfb221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
"080110011aa40b080112dc0a080410011a465354415349554e2054454d504548202854504529207c504155442d4b422053454b4152204152554d7c204a616c7572204d617469204c756d616a616e672d506173697269616e22ac095374617369756e2054656d706568202854504529206b6574696e676769616e202b3933206d206d65727570616b616e207374617369756e206b657265746120617069206d61746920286e6f6e20616b746966292079616e67207465726c6574616b20646920447573756e2054756c757372656a6f20492054656d706568204c6f72204b6563616d6174616e2054656d7065682c204b616275706174656e204c756d616a616e672064656e67616e204b6f6f7264696e6174203a203038c2b03131e280b235372e32e280b34c532c313133c2b03130e280b232392e38e280b342542e205374617369756e20696e69206d65727570616b616e2073616c61682073617475207374617369756e2070616461206a616c7572206b657265746120617069204c756d616a616e672d506173697269616e2079616e67206d756c616920646967756e616b616e2074616e6767616c203136204d65692031383936202064616e2074656c616820646974757475702073656d656e6a616b203120466562727561726920313938382e2050616461206d6173612070656e6a616a6168616e2042656c616e64612c206a616c75722d6a616c757220696e69206265726164612064692062617761682070656e67656c6f6c61616e2053746161747373706f6f722d20656e205472616d776567656e20284f6f737465726c696a6e656e29202853532d4f4c292e0a5361617420696e69206b6f6e64697369205374617369756e2054656d7065682074696e6767616c2062616e67756e616e207574616d612c2079616e672064696a6164696b616e20736562616761692074656d706174206265726d61696e206b616e616b2d6b616e616b205041554420e28093204b422053454b4152204152554d2e0a4a616c7572206b65726574612061706920696e692070616461206d617361206c616c75206d65727570616b616e206a616c75722079616e672063756b757020736962756b2c2064656e67616e205374617369756e204c756d616a616e67202d79616e67207465726265736172206469206a616c757220696e69206d656c6179616e692068616d706972203330302e3030302070656e756d70616e6720706572746168756e2064616e20626172616e672068696e676761203233207269627520746f6e206c6562696820646920616e7461726120746168756e20313935302d31393533202e204a756d6c61682070656e756d70616e672079616e67206e61696b2064617269205374617369756e2054656d7065682068616d70697220736574656e6761682062616e79616b6e79612079616e67206e61696b2064617269205374617369756e204c756d616a616e672e0a53656d656e74617261206a6172696e67616e2072656c2062657365727461206b656c656e676b6170616e20776573656c2064616e2070657273696e79616c616e6e79612074656c61682068616269732074616b20626572736973612e2042656b6173206a616c75722072656c6e7961206b696e69206d656e6a616469206a616c616e206b6563696c202867616e672920616e746172206b616d70756e672e202857696b697065646961290a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d43704e33485f6f67695f6f2a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f43704e33485f6f67695f6f52005a001a41080110011a30c11a6c72dc5cf5bb9b80bb58e760893984010a219702062234ef6eb9ec9572353a3c1b5b4da91a57057ee671b454f3c22209766964656f2f6d70342a5c080110031a40a8134e7e6e123c0b9ca568d95d8804da8a70877bdc47ca9b1c536db3a0e35a0de213ee66e3df77d42fb0c47cbd32c901b344b3e017f355169d7f85722a124dc9221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
"080110011a8307080112bb06080410011a484b4120534552415955202032313520444154414e47204449205354415349554e2042414e4a415220204d454d424157412050554c414e47207c4a75727573616e205057542d505345228905536161742073656c657361692070656e656c75737572616e206a616c7572206e6f6e20616b7469662042616e6a61722c2050616e67616e646172616e2073616d7061692043696a756c616e67206d61756e79612074656d75616e206469207374617369756e2042616e6a617220756e74756b206d656e67756361706b616e2073616c616d207065727069736168616e2064656e67616e204f6d204d6179626920507261626f776f2073616e67206d617374657220626c7573756b616e206a616c7572206e6f6e20616b7469662c207465726e79617461204b412079616e6720616b616e206d656d626177612070756c616e672062616c696b206b65204a616b6172746120646174616e672064756c75616e206461726920507572776f6b6572746f2064616e20736179612073656e64697269206d617369682062657261646120646961746173206a656d626174616e2f4f7665727061732079616e672062657261646120646920736562656c6168207374617369756e2042616e6a61722061726168204a616b617274612c20616b6869726e79612073616c616d207065727069736168616e2068616e79612064656e67616e206d656d766964696f6b616e206b65726574612079616e67206d656d626177616e79612070756c616e672062616c696b206b65204a616b6172746120646172692061746173204a656d626174616e2f4f7665727061732e0a536179612073656e646972692062616c696b206b652053757261626179612064656e67616e204b4120506173756e64616e2c20476f6f64627965206d7920667269656e6420746f206d65657420616761696e2e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d596c6f594d3353447430452a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f596c6f594d33534474304552005a001a41080110011a308c591efe76bd6d31b39c553996f925b3002b6fc150116f0e8d6bf7654e6674c5b3a59baef24c50fa908580a02dd90ded2209766964656f2f6d70342a5c080110031a40cbef89584d26bbf2695e039a10f2b34749843d827323c530e63b0472407fc7b184d174634d91f05efee9b90c1706e319bd9641226728524952e2b9004400684d221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
"080110011aed03080112a503080410011a3b4b41205345524159552020323136204449204a504c203432362d41205354415349554e2042414e4a4152207c4a75727573616e205053452d505754228002496e696c6168204b4120536572617975203231362079616e67206d656d62617761206d617374657220626c7573756b616e206a616c7572206e6f6e20616b746966204f6d204d6179626920507261626f776f202068747470733a2f2f7777772e796f75747562652e636f6d2f6368616e6e656c2f55435076355953496f59716f38364a525f4871626b437251202e0a5361617420696e6920616b616e206d656e656c7573757269206a616c7572206e6f6e20616b7469662042616e6a61722d50616e67616e646172616e2d43696a756c616e672e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4a6b4347615473774c35632a1042616d62616e6720536574796177616e321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f4a6b4347615473774c356352005a001a41080110011a302ed97c79df5eccb145f8f8e1e866be1a392004a6794347c08c7e851c5f00b1504092a9f3c0674c78805a73a33c8b1bf32209766964656f2f6d70342a5c080110031a40cbcec20908e60b5f6198aecc192d2a9e4b069aa58d9238cb7154e37c4d04f268feefe92c2705c14009acf32e7e876df180cff3afdea6c989e75b4861150d1644221402b1839207e2a706f0ba73dec0ce6b719043293d",
|
||||
}
|
||||
|
||||
func TestDecodeClaim(t *testing.T) {
|
||||
claimHex := "000aa4010a8a010a30f1303989f58396694b0c5982c97f7e9d9435841d92aa13f4b80f671c27110c469babc4fbf4bd764155eaac089cfc49e8121454554d205045204d45524e45204c41472e6d703418cad0c8012209766964656f2f6d70343230c2c9389731e2a9568f66c78d703736a8c341015ada2e46f5dcc87aa6f08ab17c02df2121d9f6ef74055827a29dfc75801a044e6f6e6532040803180a5a0908b001109001188102421054554d205045204d45524e45204c41474a0944657369206c6f636b62020801"
|
||||
claim, err := DecodeClaimHex(claimHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err, claim.ClaimID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeClaims(t *testing.T) {
|
||||
for _, claim_hex := range raw_claims {
|
||||
claim, err := DecodeClaimHex(claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
serializedHex, err := claim.serializedHexString()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if serializedHex != claim_hex {
|
||||
t.Error("failed to re-serialize")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripSignature(t *testing.T) {
|
||||
claimHex := raw_claims[1]
|
||||
claim, err := DecodeClaimHex(claimHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
noSig, err := claim.serializedNoSignature()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if hex.EncodeToString(noSig) != raw_claims[2] {
|
||||
t.Error("failed to remove signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelClaim(t *testing.T) {
|
||||
private, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
pubKeyBytes, err := keys.PublicKeyToDER(private.PubKey())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
claim := &StakeHelper{Claim: newChannelClaim(), Version: NoSig}
|
||||
claim.Claim.GetChannel().PublicKey = pubKeyBytes
|
||||
claim.Claim.Title = "Test Channel Title"
|
||||
claim.Claim.Description = "Test Channel Description"
|
||||
claim.Claim.GetChannel().Cover = &pb.Source{Url: "http://testcoverurl.com"}
|
||||
claim.Claim.Tags = []string{"TagA", "TagB", "TagC"}
|
||||
claim.Claim.Languages = []*pb.Language{{Language: pb.Language_en}, {Language: pb.Language_es}}
|
||||
claim.Claim.Thumbnail = &pb.Source{Url: "http://thumbnailurl.com"}
|
||||
claim.Claim.GetChannel().WebsiteUrl = "http://homepageurl.com"
|
||||
claim.Claim.Locations = []*pb.Location{{Country: pb.Location_AD}, {Country: pb.Location_US, State: "NJ", City: "some city"}}
|
||||
|
||||
rawClaim, err := claim.CompileValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
claim, err = DecodeClaimBytes(rawClaim, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if bytes, err := claim.CompileValue(); err != nil || len(bytes) != len(rawClaim) {
|
||||
t.Error("decoded claim does not match original")
|
||||
}
|
||||
|
||||
}
|
197
schema/stake/migration.go
Normal file
197
schema/stake/migration.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
v1pb "github.com/lbryio/types/v1/go"
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/btcsuite/btcutil/base58"
|
||||
)
|
||||
|
||||
const lbrySDHash = "lbry_sd_hash"
|
||||
|
||||
func newStreamClaim() *pb.Claim {
|
||||
claimStream := new(pb.Claim_Stream)
|
||||
stream := new(pb.Stream)
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = claimStream
|
||||
claimStream.Stream = stream
|
||||
|
||||
return pbClaim
|
||||
}
|
||||
|
||||
func newChannelClaim() *pb.Claim {
|
||||
claimChannel := new(pb.Claim_Channel)
|
||||
channel := new(pb.Channel)
|
||||
|
||||
pbClaim := new(pb.Claim)
|
||||
pbClaim.Type = claimChannel
|
||||
claimChannel.Channel = channel
|
||||
|
||||
return pbClaim
|
||||
}
|
||||
|
||||
func setMetaData(claim *pb.Claim, author string, description string, language pb.Language_Language, license string,
|
||||
licenseURL *string, title string, thumbnail *string, nsfw bool) {
|
||||
claim.Title = title
|
||||
claim.Description = description
|
||||
|
||||
claim.GetStream().Author = author
|
||||
claim.Languages = []*pb.Language{{Language: language}}
|
||||
|
||||
if thumbnail != nil {
|
||||
source := new(pb.Source)
|
||||
source.Url = *thumbnail
|
||||
claim.Thumbnail = source
|
||||
}
|
||||
if nsfw {
|
||||
claim.Tags = []string{"mature"}
|
||||
}
|
||||
claim.GetStream().License = license
|
||||
if licenseURL != nil {
|
||||
claim.GetStream().LicenseUrl = *licenseURL
|
||||
}
|
||||
}
|
||||
|
||||
func migrateV1PBClaim(vClaim v1pb.Claim) (*pb.Claim, error) {
|
||||
if *vClaim.ClaimType == v1pb.Claim_streamType {
|
||||
return migrateV1PBStream(vClaim)
|
||||
}
|
||||
if *vClaim.ClaimType == v1pb.Claim_certificateType {
|
||||
return migrateV1PBChannel(vClaim)
|
||||
}
|
||||
return nil, errors.Err("Could not migrate v1 protobuf claim due to unknown type '%s'.", vClaim.ClaimType.String())
|
||||
}
|
||||
|
||||
func migrateV1PBStream(vClaim v1pb.Claim) (*pb.Claim, error) {
|
||||
claim := newStreamClaim()
|
||||
source := new(pb.Source)
|
||||
source.MediaType = vClaim.GetStream().GetSource().GetContentType()
|
||||
source.SdHash = vClaim.GetStream().GetSource().GetSource()
|
||||
claim.GetStream().Source = source
|
||||
md := vClaim.GetStream().GetMetadata()
|
||||
if md.GetFee() != nil {
|
||||
claim.GetStream().Fee = new(pb.Fee)
|
||||
claim.GetStream().GetFee().Amount = uint64(*md.GetFee().Amount * 100000000)
|
||||
claim.GetStream().GetFee().Address = md.GetFee().GetAddress()
|
||||
claim.GetStream().GetFee().Currency = pb.Fee_Currency(pb.Fee_Currency_value[md.GetFee().GetCurrency().String()])
|
||||
}
|
||||
if vClaim.GetStream().GetMetadata().GetNsfw() {
|
||||
claim.Tags = []string{"mature"}
|
||||
}
|
||||
thumbnailSource := new(pb.Source)
|
||||
thumbnailSource.Url = md.GetThumbnail()
|
||||
claim.Thumbnail = thumbnailSource
|
||||
language := pb.Language_Language(pb.Language_Language_value[md.GetLanguage().String()])
|
||||
claim.Languages = []*pb.Language{{Language: language}}
|
||||
claim.GetStream().LicenseUrl = md.GetLicenseUrl()
|
||||
claim.GetStream().License = md.GetLicense()
|
||||
claim.Title = md.GetTitle()
|
||||
claim.Description = md.GetDescription()
|
||||
claim.GetStream().Author = md.GetAuthor()
|
||||
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func migrateV1PBChannel(vClaim v1pb.Claim) (*pb.Claim, error) {
|
||||
claim := newChannelClaim()
|
||||
claim.GetChannel().PublicKey = vClaim.GetCertificate().PublicKey
|
||||
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func migrateV1Claim(vClaim V1Claim) (*pb.Claim, error) {
|
||||
pbClaim := newStreamClaim()
|
||||
//Stream
|
||||
// -->Universal
|
||||
setFee(vClaim.Fee, pbClaim)
|
||||
// -->MetaData
|
||||
language := pb.Language_Language(pb.Language_Language_value[vClaim.Language])
|
||||
setMetaData(pbClaim, vClaim.Author, vClaim.Description, language,
|
||||
vClaim.License, nil, vClaim.Title, vClaim.Thumbnail, false)
|
||||
// -->Source
|
||||
source := new(pb.Source)
|
||||
source.MediaType = vClaim.ContentType
|
||||
|
||||
src, err := hex.DecodeString(vClaim.Sources.LbrySDHash)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
source.SdHash = src
|
||||
pbClaim.GetStream().Source = source
|
||||
|
||||
return pbClaim, nil
|
||||
}
|
||||
|
||||
func migrateV2Claim(vClaim V2Claim) (*pb.Claim, error) {
|
||||
pbClaim := newStreamClaim()
|
||||
//Stream
|
||||
// -->Fee
|
||||
setFee(vClaim.Fee, pbClaim)
|
||||
// -->MetaData
|
||||
language := pb.Language_Language(pb.Language_Language_value[vClaim.Language])
|
||||
setMetaData(pbClaim, vClaim.Author, vClaim.Description, language,
|
||||
vClaim.License, vClaim.LicenseURL, vClaim.Title, vClaim.Thumbnail, vClaim.NSFW)
|
||||
// -->Source
|
||||
source := new(pb.Source)
|
||||
source.MediaType = vClaim.ContentType
|
||||
src, err := hex.DecodeString(vClaim.Sources.LbrySDHash)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
source.SdHash = src
|
||||
pbClaim.GetStream().Source = source
|
||||
|
||||
return pbClaim, nil
|
||||
}
|
||||
|
||||
func migrateV3Claim(vClaim V3Claim) (*pb.Claim, error) {
|
||||
pbClaim := newStreamClaim()
|
||||
//Stream
|
||||
// -->Fee
|
||||
setFee(vClaim.Fee, pbClaim)
|
||||
// -->MetaData
|
||||
language := pb.Language_Language(pb.Language_Language_value[vClaim.Language])
|
||||
setMetaData(pbClaim, vClaim.Author, vClaim.Description, language,
|
||||
vClaim.License, vClaim.LicenseURL, vClaim.Title, vClaim.Thumbnail, vClaim.NSFW)
|
||||
// -->Source
|
||||
source := new(pb.Source)
|
||||
source.MediaType = vClaim.ContentType
|
||||
src, err := hex.DecodeString(vClaim.Sources.LbrySDHash)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
source.SdHash = src
|
||||
pbClaim.GetStream().Source = source
|
||||
|
||||
return pbClaim, nil
|
||||
}
|
||||
|
||||
func setFee(fee *Fee, pbClaim *pb.Claim) {
|
||||
if fee != nil {
|
||||
amount := float32(0.0)
|
||||
currency := pb.Fee_LBC
|
||||
address := ""
|
||||
if fee.BTC != nil {
|
||||
amount = fee.BTC.Amount
|
||||
currency = pb.Fee_BTC
|
||||
address = fee.BTC.Address
|
||||
} else if fee.LBC != nil {
|
||||
amount = fee.LBC.Amount
|
||||
currency = pb.Fee_LBC
|
||||
address = fee.LBC.Address
|
||||
} else if fee.USD != nil {
|
||||
amount = fee.USD.Amount
|
||||
currency = pb.Fee_USD
|
||||
address = fee.USD.Address
|
||||
}
|
||||
pbClaim.GetStream().Fee = new(pb.Fee)
|
||||
//Fee Settings
|
||||
pbClaim.GetStream().GetFee().Amount = uint64(amount * 100000000)
|
||||
pbClaim.GetStream().GetFee().Currency = currency
|
||||
pbClaim.GetStream().GetFee().Address = base58.Decode(address)
|
||||
}
|
||||
}
|
282
schema/stake/migration_test.go
Normal file
282
schema/stake/migration_test.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/schema/address"
|
||||
|
||||
"github.com/btcsuite/btcutil/base58"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
type valueTestPair struct {
|
||||
ValueAsHex string
|
||||
Claim claimResult
|
||||
}
|
||||
|
||||
type claimResult struct {
|
||||
Version string
|
||||
Author string
|
||||
Title string
|
||||
Description string
|
||||
License string
|
||||
LicenseURL string
|
||||
FeeAmount float32
|
||||
FeeCurrency string
|
||||
FeeAddress string
|
||||
ContentType string
|
||||
Language string
|
||||
LbrySDHash string
|
||||
Thumbnail string
|
||||
NSFW bool
|
||||
}
|
||||
|
||||
var badJsonVersionTests = []string{
|
||||
"7b22666565223a20352c2022766572223a2022302e302e33222c20226c6963656e7365223a202247504c20332e30222c20226c616e6775616765223a2022656e222c2022617574686f72223a2022576f6e64657220576f6d616e2031393933222c20227469746c65223a2022576f6e64657220576f6d616e2031393933222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022323338663832363932356462323235353863646335636430373436303637626666393964636263333834333832666661303236646463373230373261366362323461393332346461373961626136643232373437313164653035353063396534227d2c20226e736677223a20747275652c2022636f6e74656e745f74797065223a2022696d6167652f6a706567222c20227468756d626e61696c223a2022687474703a2f2f692e696d6775722e636f6d2f4568344a4658732e6a7067222c20226465736372697074696f6e223a2022466f7220746865206c6f766521227d",
|
||||
"7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c2022617574686f72223a20225061756c204b6176616e616768222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022393962383766363064643136643730316538613562666238353130633938343239623866633563656538623764663333643665666135386464313133313261333665616437303638643133636364636331383563376465613730643930393261227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d",
|
||||
"080110011ac3bd01080112c2b501080410011a0b57696e205465737420313122065465737420322a0444617665322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e697465642053746174657338004224080110031a19556bc2a9c28bc2b057c3aec2901d5445c3b17cc384c3bd29c3b72b5f74101cc2aec2aac2a1250000c2b2424a0052005a3868747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f64651a41080110011a302a1ec2a42cc383223f7cc2b4c291353d0f687675c289c3a34cc3a73942c383c3abc3b7330dc3912d162ac388c3bec3a5c2aec293c2b1c38f68c3a2c2bfc2907a04c3b8c39dc2b1742209696d6167652f706e67",
|
||||
"7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c2022617574686f72223a20225061756c204b6176616e616768222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022393962383766363064643136643730316538613562666238353130633938343239623866633563656538623764663333643665666135386464313133313261333665616437303638643133636364636331383563376465613730643930393261227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d",
|
||||
"7b22666565223a2022302e303031222c2022766572223a2022302e302e33222c20226465736372697074696f6e223a20224120717569636b206c6f6f6b2061742074686520536f6e79204c44502033363030204c617365726469736320706c61796572222c20226c6963656e7365223a20224c42525920696e63222c2022617574686f72223a20225061756c204b6176616e616768222c20227469746c65223a2022536f6e79204c44502033363030204c617365726469736320506c61796572222c20226c616e6775616765223a2022656e222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022626639663033333464623237393634303730663035636637356262376131626664663431363864356565376130633866373563663661623536653965633363643039633534636636383262353366316531353030363930616165353530663134227d2c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226e736677223a2066616c73657d",
|
||||
"30383031313030313161356330383031313231343038303431303030316130303232303032613030333230303338303034613030353230303561303031613432303830313130303131613330333933333030616561386262623636613238613966633031316231666238373634636364386338323633656233343532623733303832363233656534356431363866646530343836346435303563376535396539353430636263643766623336323230613639366436313637363532663661373036353637",
|
||||
}
|
||||
|
||||
var jsonVersionTests = []valueTestPair{
|
||||
{"7b22666565223a207b224c4243223a207b22616d6f756e74223a20312e302c202261646472657373223a2022625077474139683775696a6f79357541767a565051773951794c6f595a6568484a6f227d7d2c20226465736372697074696f6e223a2022313030304d4220746573742066696c6520746f206d65617375726520646f776e6c6f6164207370656564206f6e204c627279207032702d6e6574776f726b2e222c20226c6963656e7365223a20224e6f6e65222c2022617574686f72223a2022726f6f74222c20226c616e6775616765223a2022456e676c697368222c20227469746c65223a2022313030304d4220737065656420746573742066696c65222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022626439343033336431336634663339303837303837303163616635363562666130396366616466326633346661646634613733666238366232393564316232316137653634383035393934653435623566626336353066333062616334383734227d2c2022636f6e74656e742d74797065223a20226170706c69636174696f6e2f6f637465742d73747265616d222c20227468756d626e61696c223a20222f686f6d65726f626572742f6c6272792f73706565642e6a7067227d",
|
||||
claimResult{"0.0.1",
|
||||
"root",
|
||||
"1000MB speed test file",
|
||||
"1000MB test file to measure download speed on Lbry p2p-network.",
|
||||
"None",
|
||||
"",
|
||||
1,
|
||||
"LBC",
|
||||
"bPwGA9h7uijoy5uAvzVPQw9QyLoYZehHJo",
|
||||
"application/octet-stream",
|
||||
"", //"English" is not supported for conversion.
|
||||
"bd94033d13f4f3908708701caf565bfa09cfadf2f34fadf4a73fb86b295d1b21a7e64805994e45b5fbc650f30bac4874",
|
||||
"/homerobert/lbry/speed.jpg",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b22666565223a207b224c4243223a207b22616d6f756e74223a2035302e302c202261646472657373223a2022624c5673336966507275795a6e70596d46665432544c416d68715a76676a70514461227d7d2c20226465736372697074696f6e223a2022466f757220636f75706c6573206d65657420666f722053756e646179206272756e6368206f6e6c7920746f20646973636f76657220746865792061726520737475636b20696e206120686f75736520746f6765746865722061732074686520776f726c64206d61792062652061626f757420746f20656e642e222c20226c6963656e7365223a20224f7363696c6c6f73636f7065204c61626f7261746f72696573222c2022617574686f72223a20225772697474656e20616e6420646972656374656420627920546f646420426572676572222c20226c616e6775616765223a2022656e222c20227469746c65223a2022497427732061204469736173746572222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022646363316266323838393361353033376561623965346139636437613462666536663736616436633231393730656134636565653031323266353032656630373936353764346463613435366234626533323439383439633465313836386238227d2c2022636f6e74656e742d74797065223a2022766964656f2f717569636b74696d65222c20227468756d626e61696c223a2022687474703a2f2f69612e6d656469612d696d64622e636f6d2f696d616765732f4d2f4d5635424d5451774e6a597a4d5451304d6c35424d6c3542616e426e586b46745a5463774e44557a4f444d354e7740402e5f56315f5359313030305f4352302c302c3637332c313030305f414c5f2e6a7067227d",
|
||||
claimResult{"0.0.1",
|
||||
"Written and directed by Todd Berger",
|
||||
"It's a Disaster",
|
||||
"Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.",
|
||||
"Oscilloscope Laboratories",
|
||||
"",
|
||||
50,
|
||||
"LBC",
|
||||
"bLVs3ifPruyZnpYmFfT2TLAmhqZvgjpQDa",
|
||||
"video/quicktime",
|
||||
"language:en ",
|
||||
"dcc1bf28893a5037eab9e4a9cd7a4bfe6f76ad6c21970ea4ceee0122f502ef079657d4dca456b4be3249849c4e1868b8",
|
||||
"http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b226465736372697074696f6e223a202241647669736f7220416c6578205461626172726f6b206769766573206869732074616b65206f6e204c4252592e222c20226c6963656e7365223a20224c4252592c20496e632e222c2022617574686f72223a202253616d75656c20427279616e222c20226c616e6775616765223a2022656e222c20227469746c65223a20224d65657420746865205465616d20457069736f64652031222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022373939613164653933623865353536643766363638313033613666666334386163356664363830316464346438396361653637373363383064383163323833373130666434666432356564363864306462656565323638663832393134313435227d2c2022636f6e74656e742d74797065223a2022766964656f2f6d7034227d",
|
||||
claimResult{"0.0.1",
|
||||
"Samuel Bryan",
|
||||
"Meet the Team Episode 1",
|
||||
"Advisor Alex Tabarrok gives his take on LBRY.",
|
||||
"LBRY, Inc.",
|
||||
"",
|
||||
0,
|
||||
"UNKNOWN_CURRENCY",
|
||||
"",
|
||||
"video/mp4",
|
||||
"language:en ",
|
||||
"799a1de93b8e556d7f668103a6ffc48ac5fd6801dd4d89cae6773c80d81c283710fd4fd25ed68d0dbeee268f82914145",
|
||||
"http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b226c616e6775616765223a2022656e222c2022666565223a207b22555344223a207b22616d6f756e74223a20302e30312c202261646472657373223a2022624d486d5a4b5a6250713662504245514663384d5870694468463966374d56784d52227d7d2c2022736f7572636573223a207b226c6272795f73645f68617368223a2022326264386439646431613231386337663536373137653533666135313065666435613863303839656431663236373561306638643062356238626233633165643338336362396633616562396238393137383937363133303532393339373961227d2c20226465736372697074696f6e223a2022636c6f756473222c20226c6963656e7365223a2022637265617469766520636f6d6d6f6e73222c2022617574686f72223a202268747470733a2f2f7777772e76696465657a792e636f6d2f636c6f7564732f323637362d6461726b2d73746f726d2d636c6f7564732d726f79616c74792d667265652d68642d73746f636b2d766964656f222c20226e736677223a2066616c73652c20227469746c65223a2022636c6f756473222c2022636f6e74656e742d74797065223a2022766964656f2f6d7034222c2022766572223a2022302e302e32227d",
|
||||
claimResult{"0.0.2",
|
||||
"https://www.videezy.com/clouds/2676-dark-storm-clouds-royalty-free-hd-stock-video",
|
||||
"clouds",
|
||||
"clouds",
|
||||
"creative commons",
|
||||
"",
|
||||
0.01,
|
||||
"USD",
|
||||
"bMHmZKZbPq6bPBEQFc8MXpiDhF9f7MVxMR",
|
||||
"video/mp4",
|
||||
"language:en ",
|
||||
"2bd8d9dd1a218c7f56717e53fa510efd5a8c089ed1f2675a0f8d0b5b8bb3c1ed383cb9f3aeb9b891789761305293979a",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b226c6963656e7365223a2022437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e6974656420537461746573222c2022666565223a207b224c4243223a207b22616d6f756e74223a20312c202261646472657373223a20226256507157775966766a424859426f7575766b6e62514d58555a46764c644573354d227d7d2c2022766572223a2022302e302e32222c20226465736372697074696f6e223a20227a222c20226c616e6775616765223a2022656e222c2022617574686f72223a202279222c20227469746c65223a202278222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022376332316565323337333234653561353061333432353632306665366363343030643363636363303535313938363763646131623963313061393737313934653331323030343134643837313436626666343730626162376637643735343738227d2c20226e736677223a2066616c73652c20226c6963656e73655f75726c223a202268747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f6465222c2022636f6e74656e742d74797065223a2022746578742f706c61696e227d",
|
||||
claimResult{"0.0.2",
|
||||
"y",
|
||||
"x",
|
||||
"z",
|
||||
"Creative Commons Attribution 3.0 United States",
|
||||
"https://creativecommons.org/licenses/by/3.0/us/legalcode",
|
||||
1,
|
||||
"LBC",
|
||||
"bVPqWwYfvjBHYBouuvknbQMXUZFvLdEs5M",
|
||||
"text/plain",
|
||||
"language:en ",
|
||||
"7c21ee237324e5a50a3425620fe6cc400d3cccc05519867cda1b9c10a977194e31200414d87146bff470bab7f7d75478",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b22666565223a207b22555344223a207b22616d6f756e74223a20302e342c202261646472657373223a202262485365334b417674565352346d365331317a6475754639584874777363446a6f45227d7d2c2022766572223a2022302e302e33222c20226c6963656e7365223a2022437265617469766520436f6d6d6f6e73204174747269627574696f6e20332e3020556e6974656420537461746573222c20226c616e6775616765223a2022656e222c20227469746c65223a2022526561647920506c61796572204f6e652028417564696f626f6f6b2031206f66203229222c2022617574686f72223a202245726e65737420436c696e65222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022333430653164646130653834313463323166616662623166323866326338623338343832316665306431653261373134336134383130353435653634653236613431303530343264346434376463393735346236313865636466653064313931227d2c20226e736677223a2066616c73652c2022636f6e74656e745f74797065223a2022617564696f2f6d706567222c20226c6963656e73655f75726c223a202268747470733a2f2f6372656174697665636f6d6d6f6e732e6f72672f6c6963656e7365732f62792f332e302f75732f6c6567616c636f6465222c20227468756d626e61696c223a2022687474703a2f2f692e696d6775722e636f6d2f6c794b45485a632e6a7067222c20226465736372697074696f6e223a2022496e20746865207965617220323034342c2074686520776f726c64206973206772697070656420627920616e20656e65726779206372697369732063617573696e67207769646573707265616420736f6369616c2070726f626c656d7320616e642065636f6e6f6d696320737461676e6174696f6e2e20546865207072696d6172792065736361706520666f72206d6f73742070656f706c6520697320746865204f415349532c2061207669727475616c20756e6976657273652c20616363657373656420776974682061207669736f7220616e642068617074696320676c6f7665732e2049742066756e6374696f6e7320626f746820617320616e204d4d4f52504720616e642061732061207669727475616c20736f63696574792c2077697468206974732063757272656e6379206265696e6720746865206d6f737420737461626c652063757272656e637920696e2074686520776f726c642e204974207761732063726561746564206279204a616d65732048616c6c696461792c2077686f73652077696c6c206c656674206120736572696573206f6620636c75657320746f776172647320616e20456173746572204567672077697468696e20746865204f41534953207468617420776f756c64206772616e742077686f6576657220666f756e6420697420626f74682068697320666f7274756e6520616e6420636f6e74726f6c206f6620746865204f4153495320697473656c662e205468697320686173206c656420746f20616e20696e74656e736520696e74657265737420696e20616c6c2061737065637473206f662038307320706f702063756c747572652c2077686963682048616c6c69646179206d61646520636c65617220776f756c6420626520657373656e7469616c20746f2066696e64696e6720686973206567672e227d",
|
||||
claimResult{"0.0.3",
|
||||
"Ernest Cline",
|
||||
"Ready Player One (Audiobook 1 of 2)",
|
||||
"In the year 2044, the world is gripped by an energy crisis causing widespread social problems and economic stagnation. The primary escape for most people is the OASIS, a virtual universe, accessed with a visor and haptic gloves. It functions both as an MMORPG and as a virtual society, with its currency being the most stable currency in the world. It was created by James Halliday, whose will left a series of clues towards an Easter Egg within the OASIS that would grant whoever found it both his fortune and control of the OASIS itself. This has led to an intense interest in all aspects of 80s pop culture, which Halliday made clear would be essential to finding his egg.",
|
||||
"Creative Commons Attribution 3.0 United States",
|
||||
"https://creativecommons.org/licenses/by/3.0/us/legalcode",
|
||||
0.4,
|
||||
"USD",
|
||||
"bHSe3KAvtVSR4m6S11zduuF9XHtwscDjoE",
|
||||
"audio/mpeg",
|
||||
"language:en ",
|
||||
"340e1dda0e8414c21fafbb1f28f2c8b384821fe0d1e2a7143a4810545e64e26a4105042d4d47dc9754b618ecdfe0d191",
|
||||
"http://i.imgur.com/lyKEHZc.jpg",
|
||||
false,
|
||||
},
|
||||
},
|
||||
{"7b22666565223a207b224c4243223a207b22616d6f756e74223a20312c202261646472657373223a202262525478744355706a3654764a48675763527347634861467972524c6b6b69586747227d7d2c2022766572223a2022302e302e33222c20226c6963656e7365223a2022436f707972696768742032303136206272617a7a657273222c20226c616e6775616765223a2022656e222c20227469746c65223a2022686f7420706f726e222c2022617574686f72223a20226272617a7a657273222c2022736f7572636573223a207b226c6272795f73645f68617368223a2022666330646163356366633532363335343936336666313736396636653733396334653432613037393034323066666162396661336236343031653933616535613035313565666639363066386339633237323930376564613666636261323534227d2c20226e736677223a20747275652c2022636f6e74656e745f74797065223a2022766964656f2f6d7034222c20226465736372697074696f6e223a2022686f7420706f726e5c6e6272617a7a657273227d",
|
||||
claimResult{"0.0.3",
|
||||
"brazzers",
|
||||
"hot porn",
|
||||
"hot porn\nbrazzers",
|
||||
"Copyright 2016 brazzers",
|
||||
"",
|
||||
1,
|
||||
"LBC",
|
||||
"bRTxtCUpj6TvJHgWcRsGcHaFyrRLkkiXgG",
|
||||
"video/mp4",
|
||||
"language:en ",
|
||||
"fc0dac5cfc526354963ff1769f6e739c4e42a0790420ffab9fa3b6401e93ae5a0515eff960f8c9c272907eda6fcba254",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestBadMigrationFromJSON(t *testing.T) {
|
||||
for _, hexStr := range badJsonVersionTests {
|
||||
valueBytes, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = DecodeClaimBytes(valueBytes, "lbrycrd_main")
|
||||
if err == nil {
|
||||
t.Error(fmt.Sprintf("Decode error: The raw claim '%s' should have failed decoding.", hexStr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationFromJSON(t *testing.T) {
|
||||
for _, pair := range jsonVersionTests {
|
||||
valueBytes, err := hex.DecodeString(pair.ValueAsHex)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
helper, err := DecodeClaimBytes(valueBytes, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error("Decode error: ", err)
|
||||
}
|
||||
if helper.Claim.GetStream().GetAuthor() != pair.Claim.Author {
|
||||
t.Error("Author mismatch: expected", pair.Claim.Author, "got", helper.Claim.GetStream().GetAuthor())
|
||||
}
|
||||
if helper.Claim.GetTitle() != pair.Claim.Title {
|
||||
t.Error("Title mismatch: expected", pair.Claim.Title, "got '", helper.Claim.GetTitle(), "'")
|
||||
}
|
||||
if helper.Claim.GetDescription() != pair.Claim.Description {
|
||||
t.Error("Description mismatch: expected", pair.Claim.Description, "got '", helper.Claim.GetDescription(), "'")
|
||||
}
|
||||
if helper.Claim.GetStream().GetLicense() != pair.Claim.License {
|
||||
t.Error("License mismatch: expected", pair.Claim.License, "got", helper.Claim.GetStream().GetLicense())
|
||||
}
|
||||
if helper.Claim.GetStream().GetLicenseUrl() != pair.Claim.LicenseURL {
|
||||
t.Error("LicenseURL mismatch: expected", pair.Claim.LicenseURL, "got", helper.Claim.GetStream().GetLicenseUrl())
|
||||
}
|
||||
if helper.Claim.GetStream().GetFee().GetAmount() != uint64(pair.Claim.FeeAmount*100000000) {
|
||||
t.Error("Fee Amount mismatch: expected", pair.Claim.FeeAmount, "got", helper.Claim.GetStream().GetFee().GetAmount())
|
||||
}
|
||||
if helper.Claim.GetStream().GetFee().GetCurrency().String() != pair.Claim.FeeCurrency {
|
||||
t.Error("Fee Currency mismatch: expected", pair.Claim.FeeCurrency, "got", helper.Claim.GetStream().GetFee().GetCurrency())
|
||||
}
|
||||
hexaddress := base58.Encode(helper.Claim.GetStream().GetFee().GetAddress())
|
||||
if hexaddress != pair.Claim.FeeAddress {
|
||||
t.Error("Fee Address mismatch: expected", pair.Claim.FeeAddress, "got", hexaddress)
|
||||
}
|
||||
if helper.Claim.GetStream().GetSource().GetMediaType() != pair.Claim.ContentType {
|
||||
t.Error("ContentType mismatch: expected", pair.Claim.ContentType, "got", helper.Claim.GetStream().GetSource().GetMediaType())
|
||||
}
|
||||
if helper.Claim.GetLanguages()[0].String() != pair.Claim.Language {
|
||||
t.Error("Language mismatch: expected ", pair.Claim.Language, " got ", helper.Claim.GetLanguages()[0].String())
|
||||
}
|
||||
content := hex.EncodeToString(helper.Claim.GetStream().GetSource().GetSdHash())
|
||||
if content != pair.Claim.LbrySDHash {
|
||||
t.Error("Source mismatch: expected", pair.Claim.LbrySDHash, "got", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationFromV1YTSync(t *testing.T) {
|
||||
claimHex := "080110011aee04080112a604080410011a2b4865726520617265203520526561736f6e73204920e29da4efb88f204e657874636c6f7564207c20544c4722920346696e64206f7574206d6f72652061626f7574204e657874636c6f75643a2068747470733a2f2f6e657874636c6f75642e636f6d2f0a0a596f752063616e2066696e64206d65206f6e20746865736520736f6369616c733a0a202a20466f72756d733a2068747470733a2f2f666f72756d2e6865617679656c656d656e742e696f2f0a202a20506f64636173743a2068747470733a2f2f6f6666746f706963616c2e6e65740a202a2050617472656f6e3a2068747470733a2f2f70617472656f6e2e636f6d2f7468656c696e757867616d65720a202a204d657263683a2068747470733a2f2f746565737072696e672e636f6d2f73746f7265732f6f6666696369616c2d6c696e75782d67616d65720a202a205477697463683a2068747470733a2f2f7477697463682e74762f786f6e64616b0a202a20547769747465723a2068747470733a2f2f747769747465722e636f6d2f7468656c696e757867616d65720a0a2e2e2e0a68747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4672546442434f535f66632a0f546865204c696e75782047616d6572321c436f7079726967687465642028636f6e7461637420617574686f722938004a2968747470733a2f2f6265726b2e6e696e6a612f7468756d626e61696c732f4672546442434f535f666352005a001a41080110011a30040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f32209766964656f2f6d70342a5c080110031a4062b2dd4c45e364030fbfad1a6fefff695ebf20ea33a5381b947753e2a0ca359989a5cc7d15e5392a0d354c0b68498382b2701b22c03beb8dcb91089031b871e72214feb61536c007cdf4faeeaab4876cb397feaf6b51"
|
||||
claim, err := DecodeClaimHex(claimHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, claim.Claim.GetTitle() == "Here are 5 Reasons I ❤️ Nextcloud | TLG")
|
||||
assert.Assert(t, claim.Claim.GetDescription() == "Find out more about Nextcloud: https://nextcloud.com/\n\nYou can find me on these socials:\n * Forums: https://forum.heavyelement.io/\n * Podcast: https://offtopical.net\n * Patreon: https://patreon.com/thelinuxgamer\n * Merch: https://teespring.com/stores/official-linux-gamer\n * Twitch: https://twitch.tv/xondak\n * Twitter: https://twitter.com/thelinuxgamer\n\n...\nhttps://www.youtube.com/watch?v=FrTdBCOS_fc")
|
||||
assert.Assert(t, claim.GetStream().GetLicense() == "Copyrighted (contact author)")
|
||||
assert.Assert(t, claim.GetStream().GetAuthor() == "The Linux Gamer")
|
||||
//?assert.Assert(t, claim.GetStream().GetLanguages()[0])
|
||||
assert.Assert(t, claim.GetStream().GetSource().GetMediaType() == "video/mp4")
|
||||
assert.Assert(t, claim.Claim.GetThumbnail().GetUrl() == "https://berk.ninja/thumbnails/FrTdBCOS_fc")
|
||||
sdHashBytes, err := hex.DecodeString("040e8ac6e89c061f982528c23ad33829fd7146435bf7a4cc22f0bff70c4fe0b91fd36da9a375e3e1c171db825bf5d1f3")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, bytes.Equal(claim.GetStream().GetSource().GetSdHash(), sdHashBytes))
|
||||
|
||||
channelHex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae473f17"
|
||||
channel, err := DecodeClaimHex(channelHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
pubKeyBytes, err := hex.DecodeString("3056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae473f17")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, bytes.Equal(pubKeyBytes, channel.Claim.GetChannel().GetPublicKey()))
|
||||
}
|
||||
|
||||
func TestMigrationFromV1UnsignedWithFee(t *testing.T) {
|
||||
claimHex := "080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a19553f00bc139bbf40de425f94d51fffb34c1bea6d9171cd374c25000070414a0052005a001a54080110011a301f41eb0312aa7e8a5ce49349bc77d811da975833719d751523b19f123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3221c6170706c69636174696f6e2f782d7a69702d636f6d70726573736564"
|
||||
claim, err := DecodeClaimHex(claimHex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, claim.Claim.GetTitle() == "rpg midi")
|
||||
assert.Assert(t, claim.Claim.GetDescription() == "midi")
|
||||
assert.Assert(t, claim.GetStream().GetLicense() == "Creative Commons Attribution 4.0 International")
|
||||
assert.Assert(t, claim.GetStream().GetAuthor() == "rpg midi")
|
||||
//assert.Assert(t, claim.GetStream().GetLanguage() == "en")
|
||||
assert.Assert(t, claim.GetStream().GetSource().GetMediaType() == "application/x-zip-compressed")
|
||||
sdHashBytes, err := hex.DecodeString("1f41eb0312aa7e8a5ce49349bc77d811da975833719d751523b19f123fc3d528d6a94e3446ccddb7b9329f27a9cad7e3")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, bytes.Equal(claim.GetStream().GetSource().GetSdHash(), sdHashBytes))
|
||||
feeAddressBytes, err := address.DecodeAddress("bJUQ9MxS9N6M29zsA5GTpVSDzsnPjMBBX9", "lbrycrd_main")
|
||||
assert.Assert(t, bytes.Equal(claim.GetStream().GetFee().GetAddress(), feeAddressBytes[:]))
|
||||
assert.Assert(t, claim.GetStream().GetFee().GetAmount() == 1500000000)
|
||||
assert.Assert(t, claim.GetStream().GetFee().GetCurrency().String() == "LBC")
|
||||
|
||||
}
|
32
schema/stake/pretty.go
Normal file
32
schema/stake/pretty.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
)
|
||||
|
||||
func marshalToString(c *StakeHelper) (string, error) {
|
||||
m_pb := &jsonpb.Marshaler{}
|
||||
if c.IsSupport() {
|
||||
return m_pb.MarshalToString(c.Support)
|
||||
}
|
||||
return m_pb.MarshalToString(c.Claim)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) RenderJSON() (string, error) {
|
||||
r, err := marshalToString(c)
|
||||
if err != nil {
|
||||
fmt.Println("err")
|
||||
return "", err
|
||||
}
|
||||
var dat map[string]interface{}
|
||||
err = json.Unmarshal([]byte(r), &dat)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
//TODO: encode byte arrays with b58 for addresses and b16 for source hashes instead of the default of b64
|
125
schema/stake/schema.go
Normal file
125
schema/stake/schema.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
// V1Claim is the first version of claim metadata used by lbry.
|
||||
type V1Claim struct {
|
||||
Version string `json:"ver,omitempty"`
|
||||
Title string `json:"title"` //Required
|
||||
Description string `json:"description"` //Required
|
||||
Author string `json:"author"` //Required
|
||||
Language string `json:"language"` //Required
|
||||
License string `json:"license"` //Required
|
||||
Sources Sources `json:"sources"` //Required
|
||||
ContentType string `json:"content-type"` //Required
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Fee *Fee `json:"fee,omitempty"`
|
||||
Contact *int `json:"contact,omitempty"`
|
||||
PubKey *string `json:"pubkey,omitempty"`
|
||||
}
|
||||
|
||||
// V2Claim is the second version of claim metadata used by lbry.
|
||||
type V2Claim struct {
|
||||
Version string `json:"ver"` //Required
|
||||
Title string `json:"title"` //Required
|
||||
Description string `json:"description"` //Required
|
||||
Author string `json:"author"` //Required
|
||||
Language string `json:"language"` //Required
|
||||
License string `json:"license"` //Required
|
||||
Sources Sources `json:"sources"` //Required
|
||||
ContentType string `json:"content-type"` //Required
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Fee *Fee `json:"fee,omitempty"`
|
||||
Contact *int `json:"contact,omitempty"`
|
||||
PubKey *string `json:"pubkey,omitempty"`
|
||||
LicenseURL *string `json:"license_url,omitempty"`
|
||||
NSFW bool `json:"nsfw"` //Required
|
||||
|
||||
}
|
||||
|
||||
// V3Claim is the third version of claim metadata used by lbry.
|
||||
type V3Claim struct {
|
||||
Version string `json:"ver"` //Required
|
||||
Title string `json:"title"` //Required
|
||||
Description string `json:"description"` //Required
|
||||
Author string `json:"author"` //Required
|
||||
Language string `json:"language"` //Required
|
||||
License string `json:"license"` //Required
|
||||
Sources Sources `json:"sources"` //Required
|
||||
ContentType string `json:"content_type"` //Required
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Fee *Fee `json:"fee,omitempty"`
|
||||
Contact *int `json:"contact,omitempty"`
|
||||
PubKey *string `json:"pubkey,omitempty"`
|
||||
LicenseURL *string `json:"license_url,omitempty"`
|
||||
NSFW bool `json:"nsfw"` //Required
|
||||
Sig *string `json:"sig"`
|
||||
}
|
||||
|
||||
// FeeInfo is the structure of fee information used by lbry.
|
||||
type FeeInfo struct {
|
||||
Amount float32 `json:"amount"` //Required
|
||||
Address string `json:"address"` //Required
|
||||
}
|
||||
|
||||
// Sources is the structure of Sources that can be used for a claim. Sources mainly include lbrysdhash but could be from
|
||||
// elsewhere in the future.
|
||||
type Sources struct {
|
||||
LbrySDHash string `json:"lbry_sd_hash"` //Required
|
||||
BTIH string `json:"btih"` //Required
|
||||
URL string `json:"url"` //Required
|
||||
}
|
||||
|
||||
// Fee is the structure used for different currencies allowed for claims.
|
||||
type Fee struct {
|
||||
LBC *FeeInfo `json:"LBC,omitempty"`
|
||||
BTC *FeeInfo `json:"BTC,omitempty"`
|
||||
USD *FeeInfo `json:"USD,omitempty"`
|
||||
}
|
||||
|
||||
// Unmarshal is an implementation to unmarshal the V1 claim from json. Main addition is to check the version.
|
||||
func (c *V1Claim) Unmarshal(value []byte) error {
|
||||
err := json.Unmarshal(value, c)
|
||||
if err != nil {
|
||||
return err
|
||||
} //Version can be blank for version 1
|
||||
if c.Version != "" && c.Version != "0.0.1" {
|
||||
err = errors.Base("Incorrect version, expected 0.0.1 found " + c.Version)
|
||||
return err
|
||||
}
|
||||
//ToDo - restrict to required fields?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal is an implementation to unmarshal the V2 claim from json. Main addition is to check the version.
|
||||
func (c *V2Claim) Unmarshal(value []byte) error {
|
||||
err := json.Unmarshal(value, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Version != "0.0.2" {
|
||||
err = errors.Base("Incorrect version, expected 0.0.2 found " + c.Version)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal is an implementation to unmarshal the V3 claim from json. Main addition is to check the version.
|
||||
func (c *V3Claim) Unmarshal(value []byte) error {
|
||||
err := json.Unmarshal(value, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Version != "0.0.3" {
|
||||
err = errors.Base("Incorrect version, expected 0.0.3 found " + c.Version)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
105
schema/stake/serialization.go
Normal file
105
schema/stake/serialization.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
legacy "github.com/lbryio/types/v1/go"
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
func (c *StakeHelper) serialized() ([]byte, error) {
|
||||
serialized := c.Claim.String() + c.Support.String()
|
||||
if serialized == "" {
|
||||
return nil, errors.Err("not initialized")
|
||||
}
|
||||
|
||||
if c.LegacyClaim != nil {
|
||||
return proto.Marshal(c.getLegacyProtobuf())
|
||||
} else if c.IsSupport() {
|
||||
return proto.Marshal(c.getSupportProtobuf())
|
||||
}
|
||||
|
||||
return proto.Marshal(c.getClaimProtobuf())
|
||||
}
|
||||
|
||||
func (c *StakeHelper) getClaimProtobuf() *pb.Claim {
|
||||
claim := &pb.Claim{
|
||||
Title: c.Claim.GetTitle(),
|
||||
Description: c.Claim.GetDescription(),
|
||||
Thumbnail: c.Claim.GetThumbnail(),
|
||||
Tags: c.Claim.GetTags(),
|
||||
Languages: c.Claim.GetLanguages(),
|
||||
Locations: c.Claim.GetLocations(),
|
||||
}
|
||||
if c.Claim.GetChannel() != nil {
|
||||
claim.Type = &pb.Claim_Channel{Channel: c.Claim.GetChannel()}
|
||||
} else if c.GetStream() != nil {
|
||||
claim.Type = &pb.Claim_Stream{Stream: c.GetStream()}
|
||||
} else if c.Claim.GetCollection() != nil {
|
||||
claim.Type = &pb.Claim_Collection{Collection: c.Claim.GetCollection()}
|
||||
} else if c.Claim.GetRepost() != nil {
|
||||
claim.Type = &pb.Claim_Repost{Repost: c.Claim.GetRepost()}
|
||||
}
|
||||
|
||||
return claim
|
||||
}
|
||||
|
||||
func (c *StakeHelper) getSupportProtobuf() *pb.Support {
|
||||
return &pb.Support{
|
||||
Emoji: c.Support.GetEmoji(),
|
||||
XXX_NoUnkeyedLiteral: struct{}{},
|
||||
XXX_unrecognized: nil,
|
||||
XXX_sizecache: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StakeHelper) getLegacyProtobuf() *legacy.Claim {
|
||||
v := c.LegacyClaim.GetVersion()
|
||||
t := c.LegacyClaim.GetClaimType()
|
||||
return &legacy.Claim{
|
||||
Version: &v,
|
||||
ClaimType: &t,
|
||||
Stream: c.LegacyClaim.GetStream(),
|
||||
Certificate: c.LegacyClaim.GetCertificate(),
|
||||
PublisherSignature: c.LegacyClaim.GetPublisherSignature()}
|
||||
}
|
||||
|
||||
func (c *StakeHelper) serializedHexString() (string, error) {
|
||||
serialized, err := c.serialized()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
serialized_hex := hex.EncodeToString(serialized)
|
||||
return serialized_hex, nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) serializedNoSignature() ([]byte, error) {
|
||||
if c.Claim.String() == "" && c.Support.String() == "" {
|
||||
return nil, errors.Err("not initialized")
|
||||
}
|
||||
if c.Signature == nil {
|
||||
serialized, err := c.serialized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return serialized, nil
|
||||
} else {
|
||||
if c.LegacyClaim != nil {
|
||||
clone := &legacy.Claim{}
|
||||
proto.Merge(clone, c.getLegacyProtobuf())
|
||||
proto.ClearAllExtensions(clone.PublisherSignature)
|
||||
clone.PublisherSignature = nil
|
||||
return proto.Marshal(clone)
|
||||
} else if c.IsSupport() {
|
||||
clone := &pb.Support{}
|
||||
proto.Merge(clone, c.getSupportProtobuf())
|
||||
return proto.Marshal(clone)
|
||||
}
|
||||
clone := &pb.Claim{}
|
||||
proto.Merge(clone, c.getClaimProtobuf())
|
||||
return proto.Marshal(clone)
|
||||
}
|
||||
}
|
99
schema/stake/sign.go
Normal file
99
schema/stake/sign.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/address"
|
||||
"github.com/lbryio/lbry.go/v2/schema/keys"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
)
|
||||
|
||||
func Sign(privKey btcec.PrivateKey, channel StakeHelper, claim StakeHelper, k string) (*keys.Signature, error) {
|
||||
if channel.Claim.GetChannel() == nil {
|
||||
return nil, errors.Err("claim as channel is not of type channel")
|
||||
}
|
||||
if claim.LegacyClaim != nil {
|
||||
return claim.signV1(privKey, channel, k)
|
||||
}
|
||||
|
||||
return claim.sign(privKey, channel, k)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) sign(privKey btcec.PrivateKey, channel StakeHelper, firstInputTxID string) (*keys.Signature, error) {
|
||||
|
||||
txidBytes, err := hex.DecodeString(firstInputTxID)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
metadataBytes, err := c.serialized()
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
var digest []byte
|
||||
digest = append(digest, txidBytes...)
|
||||
digest = append(digest, c.ClaimID...)
|
||||
digest = append(digest, metadataBytes...)
|
||||
hash := sha256.Sum256(digest)
|
||||
hashBytes := make([]byte, len(hash))
|
||||
for i, b := range hash {
|
||||
hashBytes[i] = b
|
||||
}
|
||||
|
||||
sig, err := privKey.Sign(hashBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
return &keys.Signature{*sig}, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *StakeHelper) signV1(privKey btcec.PrivateKey, channel StakeHelper, claimAddress string) (*keys.Signature, error) {
|
||||
metadataBytes, err := c.serializedNoSignature()
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
addressBytes, err := address.DecodeAddress(claimAddress, "lbrycrd_main")
|
||||
if err != nil {
|
||||
return nil, errors.Prefix("V1 signing requires claim address and the decode failed with: ", err)
|
||||
}
|
||||
|
||||
var digest []byte
|
||||
|
||||
address := make([]byte, len(addressBytes))
|
||||
for i, b := range addressBytes {
|
||||
address[i] = b
|
||||
}
|
||||
|
||||
digest = append(digest, address...)
|
||||
digest = append(digest, metadataBytes...)
|
||||
digest = append(digest, channel.ClaimID...)
|
||||
|
||||
hash := sha256.Sum256(digest)
|
||||
hashBytes := make([]byte, len(hash))
|
||||
for i, b := range hash {
|
||||
hashBytes[i] = b
|
||||
}
|
||||
|
||||
sig, err := privKey.Sign(hashBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
|
||||
return &keys.Signature{Signature: *sig}, nil
|
||||
}
|
||||
|
||||
// rev reverses a byte slice. useful for switching endian-ness
|
||||
func reverseBytes(b []byte) []byte {
|
||||
r := make([]byte, len(b))
|
||||
for left, right := 0, len(b)-1; left < right; left, right = left+1, right-1 {
|
||||
r[left], r[right] = b[right], b[left]
|
||||
}
|
||||
return r
|
||||
}
|
196
schema/stake/sign_test.go
Normal file
196
schema/stake/sign_test.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/schema/keys"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
privateKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
channel := &StakeHelper{newChannelClaim(), nil, nil, nil, NoSig, nil, nil}
|
||||
pubkeyBytes, err := keys.PublicKeyToDER(privateKey.PubKey())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
channel.Claim.GetChannel().PublicKey = pubkeyBytes
|
||||
claimID := "cf3f7c898af87cc69b06a6ac7899efb9a4878fdb" //Fake
|
||||
txid := "4c1df9e022e396859175f9bfa69b38e444db10fb53355fa99a0989a83bcdb82f" //Fake
|
||||
claimIDHexBytes, err := hex.DecodeString(claimID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
claim := &StakeHelper{newStreamClaim(), nil, nil, reverseBytes(claimIDHexBytes), WithSig, nil, nil}
|
||||
claim.Claim.Title = "Test title"
|
||||
claim.Claim.Description = "Test description"
|
||||
sig, err := Sign(*privateKey, *channel, *claim, txid)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
signatureBytes, err := sig.LBRYSDKEncode()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
claim.Signature = signatureBytes
|
||||
|
||||
rawChannel, err := channel.CompileValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
rawClaim, err := claim.CompileValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err = DecodeClaimBytes(rawChannel, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
claim, err = DecodeClaimBytes(rawClaim, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := claim.ValidateClaimSignature(channel, txid, claimID, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Assert(t, valid, "could not verify signature")
|
||||
|
||||
}
|
||||
|
||||
func TestSignSupportWithChannel(t *testing.T) {
|
||||
cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367"
|
||||
channel, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
privateKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
pubkeyBytes, err := keys.PublicKeyToDER(privateKey.PubKey())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
channel.Claim.GetChannel().PublicKey = pubkeyBytes
|
||||
|
||||
claimID := "251305ca93d4dbedb50dceb282ebcb7b07b7ac64"
|
||||
txid := "4c1df9e022e396859175f9bfa69b38e444db10fb53355fa99a0989a83bcdb82f" //Fake
|
||||
claimIDHexBytes, err := hex.DecodeString(claimID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
support := &StakeHelper{nil, &pb.Support{}, nil, reverseBytes(claimIDHexBytes), WithSig, nil, nil}
|
||||
sig, err := Sign(*privateKey, *channel, *support, txid)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
signatureBytes, err := sig.LBRYSDKEncode()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
support.Signature = signatureBytes
|
||||
compiledSupport, err := support.CompileValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
support, err = DecodeSupportBytes(compiledSupport, "lbrycrd_main")
|
||||
|
||||
valid, err := support.ValidateClaimSignature(channel, txid, claimID, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Assert(t, valid, "could not verify signature")
|
||||
}
|
||||
|
||||
func TestSignWithV1Channel(t *testing.T) {
|
||||
cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367"
|
||||
channel, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
privateKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
pubkeyBytes, err := keys.PublicKeyToDER(privateKey.PubKey())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
channel.Claim.GetChannel().PublicKey = pubkeyBytes
|
||||
|
||||
claimID := "251305ca93d4dbedb50dceb282ebcb7b07b7ac64"
|
||||
txid := "4c1df9e022e396859175f9bfa69b38e444db10fb53355fa99a0989a83bcdb82f" //Fake
|
||||
claimIDHexBytes, err := hex.DecodeString(claimID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
claim := &StakeHelper{newStreamClaim(), nil, nil, reverseBytes(claimIDHexBytes), WithSig, nil, nil}
|
||||
claim.Claim.Title = "Test title"
|
||||
claim.Claim.Description = "Test description"
|
||||
sig, err := Sign(*privateKey, *channel, *claim, txid)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
signatureBytes, err := sig.LBRYSDKEncode()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
claim.Signature = signatureBytes
|
||||
compiledClaim, err := claim.CompileValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
claim, err = DecodeClaimBytes(compiledClaim, "lbrycrd_main")
|
||||
|
||||
valid, err := claim.ValidateClaimSignature(channel, txid, claimID, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Assert(t, valid, "could not verify signature")
|
||||
|
||||
}
|
317
schema/stake/stake.go
Normal file
317
schema/stake/stake.go
Normal file
|
@ -0,0 +1,317 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/address"
|
||||
"github.com/lbryio/lbry.go/v2/schema/keys"
|
||||
legacy_pb "github.com/lbryio/types/v1/go"
|
||||
pb "github.com/lbryio/types/v2/go"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
type version byte
|
||||
|
||||
func (v version) byte() byte {
|
||||
return byte(v)
|
||||
}
|
||||
|
||||
const (
|
||||
NoSig = version(byte(0))
|
||||
//Signature using ECDSA SECP256k1 key and SHA-256 hash.
|
||||
WithSig = version(byte(1))
|
||||
UNKNOWN = version(byte(2))
|
||||
)
|
||||
|
||||
type StakeHelper struct {
|
||||
Claim *pb.Claim
|
||||
Support *pb.Support
|
||||
LegacyClaim *legacy_pb.Claim
|
||||
ClaimID []byte
|
||||
Version version
|
||||
Signature []byte
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
const migrationErrorMessage = "migration from v1 to v2 protobuf failed with: "
|
||||
|
||||
func (c *StakeHelper) ValidateAddresses(blockchainName string) error {
|
||||
if c.Claim != nil { // V2
|
||||
// check the validity of a fee address
|
||||
if c.Claim.GetStream() != nil {
|
||||
fee := c.GetStream().GetFee()
|
||||
if fee != nil {
|
||||
return validateAddress(fee.GetAddress(), blockchainName)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if c.Claim.GetChannel() != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Err("claim helper created with migrated v2 protobuf claim 'invalid state'")
|
||||
}
|
||||
|
||||
func validateAddress(tmp_addr []byte, blockchainName string) error {
|
||||
if len(tmp_addr) != 25 {
|
||||
return errors.Err("invalid address length: " + strconv.FormatInt(int64(len(tmp_addr)), 10) + "!")
|
||||
}
|
||||
addr := [25]byte{}
|
||||
for i := range addr {
|
||||
addr[i] = tmp_addr[i]
|
||||
}
|
||||
_, err := address.EncodeAddress(addr, blockchainName)
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVersionFromByte(versionByte byte) version {
|
||||
if versionByte == byte(0) {
|
||||
return NoSig
|
||||
} else if versionByte == byte(1) {
|
||||
return WithSig
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
func (c *StakeHelper) ValidateCertificate() error {
|
||||
if !c.IsClaim() || c.Claim.GetChannel() == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := c.GetPublicKey()
|
||||
if err != nil {
|
||||
return errors.Err(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) IsClaim() bool {
|
||||
return c.Claim != nil && c.Claim.String() != ""
|
||||
}
|
||||
|
||||
func (c *StakeHelper) IsSupport() bool {
|
||||
return c.Support != nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) LoadFromBytes(raw_claim []byte, blockchainName string) error {
|
||||
return c.loadFromBytes(raw_claim, false, blockchainName)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) LoadSupportFromBytes(raw_claim []byte, blockchainName string) error {
|
||||
return c.loadFromBytes(raw_claim, true, blockchainName)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) loadFromBytes(raw_claim []byte, isSupport bool, blockchainName string) error {
|
||||
if c.Claim.String() != "" && !isSupport {
|
||||
return errors.Err("already initialized")
|
||||
}
|
||||
if len(raw_claim) < 1 {
|
||||
return errors.Err("there is nothing to decode")
|
||||
}
|
||||
|
||||
var claim_pb *pb.Claim
|
||||
var legacy_claim_pb *legacy_pb.Claim
|
||||
var support_pb *pb.Support
|
||||
|
||||
version := getVersionFromByte(raw_claim[0]) //First byte = version
|
||||
pbPayload := raw_claim[1:]
|
||||
var claimID []byte
|
||||
var signature []byte
|
||||
if version == WithSig {
|
||||
if len(raw_claim) < 85 {
|
||||
return errors.Err("signature version indicated by 1st byte but not enough bytes for valid format")
|
||||
}
|
||||
claimID = raw_claim[1:21] // channel claimid = next 20 bytes
|
||||
signature = raw_claim[21:85] // signature = next 64 bytes
|
||||
pbPayload = raw_claim[85:] // protobuf payload = remaining bytes
|
||||
}
|
||||
|
||||
var err error
|
||||
if !isSupport {
|
||||
claim_pb = &pb.Claim{}
|
||||
err = proto.Unmarshal(pbPayload, claim_pb)
|
||||
} else {
|
||||
support := &pb.Support{}
|
||||
err = proto.Unmarshal(pbPayload, support)
|
||||
if err == nil {
|
||||
support_pb = support
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
legacy_claim_pb = &legacy_pb.Claim{}
|
||||
legacyErr := proto.Unmarshal(raw_claim, legacy_claim_pb)
|
||||
if legacyErr == nil {
|
||||
claim_pb, err = migrateV1PBClaim(*legacy_claim_pb)
|
||||
if err != nil {
|
||||
return errors.Prefix(migrationErrorMessage, err)
|
||||
}
|
||||
if legacy_claim_pb.GetPublisherSignature() != nil {
|
||||
version = WithSig
|
||||
claimID = legacy_claim_pb.GetPublisherSignature().GetCertificateId()
|
||||
signature = legacy_claim_pb.GetPublisherSignature().GetSignature()
|
||||
}
|
||||
if legacy_claim_pb.GetCertificate() != nil {
|
||||
version = NoSig
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
*c = StakeHelper{
|
||||
Claim: claim_pb,
|
||||
Support: support_pb,
|
||||
LegacyClaim: legacy_claim_pb,
|
||||
ClaimID: claimID,
|
||||
Version: version,
|
||||
Signature: signature,
|
||||
Payload: pbPayload,
|
||||
}
|
||||
|
||||
// Commenting out because of a bug in SDK release allowing empty addresses.
|
||||
//err = c.ValidateAddresses(blockchainName)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
err = c.ValidateCertificate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) LoadFromHexString(claim_hex string, blockchainName string) error {
|
||||
buf, err := hex.DecodeString(claim_hex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.LoadFromBytes(buf, blockchainName)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) LoadSupportFromHexString(claim_hex string, blockchainName string) error {
|
||||
buf, err := hex.DecodeString(claim_hex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.LoadSupportFromBytes(buf, blockchainName)
|
||||
}
|
||||
|
||||
func DecodeClaimProtoBytes(serialized []byte, blockchainName string) (*StakeHelper, error) {
|
||||
claim := &StakeHelper{&pb.Claim{}, &pb.Support{}, nil, nil, NoSig, nil, nil}
|
||||
err := claim.LoadFromBytes(serialized, blockchainName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func DecodeSupportProtoBytes(serialized []byte, blockchainName string) (*StakeHelper, error) {
|
||||
claim := &StakeHelper{nil, &pb.Support{}, nil, nil, NoSig, nil, nil}
|
||||
err := claim.LoadSupportFromBytes(serialized, blockchainName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func DecodeClaimHex(serialized string, blockchainName string) (*StakeHelper, error) {
|
||||
claim_bytes, err := hex.DecodeString(serialized)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
return DecodeClaimBytes(claim_bytes, blockchainName)
|
||||
}
|
||||
|
||||
// DecodeClaimBytes take a byte array and tries to decode it to a protobuf claim or migrate it from either json v1,2,3 or pb v1
|
||||
func DecodeClaimBytes(serialized []byte, blockchainName string) (*StakeHelper, error) {
|
||||
helper, err := DecodeClaimProtoBytes(serialized, blockchainName)
|
||||
if err == nil {
|
||||
return helper, nil
|
||||
}
|
||||
helper = &StakeHelper{}
|
||||
//If protobuf fails, try json versions before returning an error.
|
||||
v1Claim := new(V1Claim)
|
||||
err = v1Claim.Unmarshal(serialized)
|
||||
if err != nil {
|
||||
v2Claim := new(V2Claim)
|
||||
err := v2Claim.Unmarshal(serialized)
|
||||
if err != nil {
|
||||
v3Claim := new(V3Claim)
|
||||
err := v3Claim.Unmarshal(serialized)
|
||||
if err != nil {
|
||||
return nil, errors.Prefix("Claim value has no matching version", err)
|
||||
}
|
||||
helper.Claim, err = migrateV3Claim(*v3Claim)
|
||||
if err != nil {
|
||||
return nil, errors.Prefix("V3 Metadata Migration Error", err)
|
||||
}
|
||||
return helper, nil
|
||||
}
|
||||
helper.Claim, err = migrateV2Claim(*v2Claim)
|
||||
if err != nil {
|
||||
return nil, errors.Prefix("V2 Metadata Migration Error ", err)
|
||||
}
|
||||
return helper, nil
|
||||
}
|
||||
|
||||
helper.Claim, err = migrateV1Claim(*v1Claim)
|
||||
if err != nil {
|
||||
return nil, errors.Prefix("V1 Metadata Migration Error ", err)
|
||||
}
|
||||
return helper, nil
|
||||
}
|
||||
|
||||
// DecodeSupportBytes take a byte array and tries to decode it to a protobuf support
|
||||
func DecodeSupportBytes(serialized []byte, blockchainName string) (*StakeHelper, error) {
|
||||
helper, err := DecodeSupportProtoBytes(serialized, blockchainName)
|
||||
if err != nil {
|
||||
return nil, errors.Err(err)
|
||||
}
|
||||
return helper, nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) GetStream() *pb.Stream {
|
||||
if c != nil {
|
||||
return c.Claim.GetStream()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) CompileValue() ([]byte, error) {
|
||||
payload, err := c.serialized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var value []byte
|
||||
value = append(value, c.Version.byte())
|
||||
if c.Version == WithSig {
|
||||
value = append(value, c.ClaimID...)
|
||||
value = append(value, c.Signature...)
|
||||
}
|
||||
value = append(value, payload...)
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) GetPublicKey() (*btcec.PublicKey, error) {
|
||||
if c.IsClaim() {
|
||||
if c.Claim.GetChannel() == nil {
|
||||
return nil, errors.Err("claim is not of type channel, so there is no public key to get")
|
||||
}
|
||||
|
||||
} else if c.IsSupport() {
|
||||
return nil, errors.Err("stake is a support and does not come with a public key to get")
|
||||
}
|
||||
return keys.GetPublicKeyFromBytes(c.Claim.GetChannel().PublicKey)
|
||||
}
|
33
schema/stake/stake_test.go
Normal file
33
schema/stake/stake_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package stake
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClaimHelper(t *testing.T) {
|
||||
for _, rawClaim := range raw_claims {
|
||||
helper, err := DecodeClaimHex(rawClaim, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = helper.RenderJSON()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = helper.serialized()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = helper.serializedHexString()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = helper.serializedNoSignature()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = helper.ValidateAddresses("lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
120
schema/stake/validator.go
Normal file
120
schema/stake/validator.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/schema/address"
|
||||
)
|
||||
|
||||
const SECP256k1 = "SECP256k1"
|
||||
|
||||
//const NIST256p = "NIST256p"
|
||||
//const NIST384p = "NIST384p"
|
||||
|
||||
func getClaimSignatureDigest(bytes ...[]byte) [32]byte {
|
||||
|
||||
var combined []byte
|
||||
for _, b := range bytes {
|
||||
combined = append(combined, b...)
|
||||
}
|
||||
digest := sha256.Sum256(combined)
|
||||
return [32]byte(digest)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) VerifyDigest(certificate *StakeHelper, signature [64]byte, digest [32]byte) bool {
|
||||
if certificate == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
R := &big.Int{}
|
||||
S := &big.Int{}
|
||||
R.SetBytes(signature[0:32])
|
||||
S.SetBytes(signature[32:64])
|
||||
pk, err := certificate.GetPublicKey()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ecdsa.Verify(pk.ToECDSA(), digest[:], R, S)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) ValidateClaimSignature(certificate *StakeHelper, k string, certificateId string, blockchainName string) (bool, error) {
|
||||
if c.LegacyClaim != nil {
|
||||
return c.validateV1ClaimSignature(certificate, k, certificateId, blockchainName)
|
||||
}
|
||||
|
||||
return c.validateClaimSignature(certificate, k, certificateId, blockchainName)
|
||||
}
|
||||
|
||||
func (c *StakeHelper) validateClaimSignature(certificate *StakeHelper, firstInputTxHash, certificateId string, blockchainName string) (bool, error) {
|
||||
certificateIdSlice, err := hex.DecodeString(certificateId)
|
||||
if err != nil {
|
||||
return false, errors.Err(err)
|
||||
}
|
||||
certificateIdSlice = reverseBytes(certificateIdSlice)
|
||||
firstInputTxIDBytes, err := hex.DecodeString(firstInputTxHash)
|
||||
if err != nil {
|
||||
return false, errors.Err(err)
|
||||
}
|
||||
|
||||
signature := c.Signature
|
||||
if signature == nil {
|
||||
return false, errors.Err("claim does not have a signature")
|
||||
}
|
||||
signatureBytes := [64]byte{}
|
||||
for i, b := range signature {
|
||||
signatureBytes[i] = b
|
||||
}
|
||||
|
||||
claimDigest := getClaimSignatureDigest(firstInputTxIDBytes, certificateIdSlice, c.Payload)
|
||||
return c.VerifyDigest(certificate, signatureBytes, claimDigest), nil
|
||||
}
|
||||
|
||||
func (c *StakeHelper) validateV1ClaimSignature(certificate *StakeHelper, claimAddy string, certificateId string, blockchainName string) (bool, error) {
|
||||
addressBytes, err := address.DecodeAddress(claimAddy, blockchainName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
//For V1 claim_id was incorrectly stored for claim signing.
|
||||
// So the bytes are not reversed like they are supposed to be (Endianess)
|
||||
certificateIdSlice, err := hex.DecodeString(certificateId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
signature := c.Signature
|
||||
if signature == nil {
|
||||
return false, errors.Err("claim does not have a signature")
|
||||
}
|
||||
signatureBytes := [64]byte{}
|
||||
for i := range signatureBytes {
|
||||
signatureBytes[i] = signature[i]
|
||||
}
|
||||
|
||||
claimAddress, err := address.ValidateAddress(addressBytes, blockchainName)
|
||||
if err != nil {
|
||||
return false, errors.Err("invalid address")
|
||||
}
|
||||
|
||||
serializedNoSig, err := c.serializedNoSignature()
|
||||
if err != nil {
|
||||
return false, errors.Err("serialization error")
|
||||
}
|
||||
|
||||
claimDigest := getClaimSignatureDigest(claimAddress[:], serializedNoSig, certificateIdSlice)
|
||||
return c.VerifyDigest(certificate, signatureBytes, claimDigest), nil
|
||||
}
|
||||
|
||||
func GetOutpointHash(txid string, vout uint32) (string, error) {
|
||||
txidBytes, err := hex.DecodeString(txid)
|
||||
if err != nil {
|
||||
return "", errors.Err(err)
|
||||
}
|
||||
var voutBytes = make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(voutBytes, vout)
|
||||
return hex.EncodeToString(append(reverseBytes(txidBytes), voutBytes...)), nil
|
||||
}
|
94
schema/stake/validator_test.go
Normal file
94
schema/stake/validator_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package stake
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestV1ValidateClaimSignature(t *testing.T) {
|
||||
cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367"
|
||||
signed_claim_hex := "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65"
|
||||
|
||||
signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
claim_addr := "bSkUov7HMWpYBiXackDwRnR5ishhGHvtJt"
|
||||
cert_id := "251305ca93d4dbedb50dceb282ebcb7b07b7ac65"
|
||||
|
||||
result, err := signed_claim.ValidateClaimSignature(cert_claim, claim_addr, cert_id, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != true {
|
||||
t.Error("failed to validate signature:", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1FailToValidateClaimSignature(t *testing.T) {
|
||||
cert_claim_hex := "08011002225e0801100322583056301006072a8648ce3d020106052b8104000a03420004d015365a40f3e5c03c87227168e5851f44659837bcf6a3398ae633bc37d04ee19baeb26dc888003bd728146dbea39f5344bf8c52cedaf1a3a1623a0166f4a367"
|
||||
signed_claim_hex := "080110011ad7010801128f01080410011a0c47616d65206f66206c696665221047616d65206f66206c696665206769662a0b4a6f686e20436f6e776179322e437265617469766520436f6d6d6f6e73204174747269627574696f6e20342e3020496e7465726e6174696f6e616c38004224080110011a195569c917f18bf5d2d67f1346aa467b218ba90cdbf2795676da250000803f4a0052005a001a41080110011a30b6adf6e2a62950407ea9fb045a96127b67d39088678d2f738c359894c88d95698075ee6203533d3c204330713aa7acaf2209696d6167652f6769662a5c080110031a40c73fe1be4f1743c2996102eec6ce0509e03744ab940c97d19ddb3b25596206367ab1a3d2583b16c04d2717eeb983ae8f84fee2a46621ffa5c4726b30174c6ff82214251305ca93d4dbedb50dceb282ebcb7b07b7ac65"
|
||||
|
||||
signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
claim_addr := "bSkUov7HMWpYBiXackDwRnR5ishhGHvtJt"
|
||||
cert_id := "251305ca93d4dbedb50dceb282ebcb7b07b7ac64"
|
||||
|
||||
result, err := signed_claim.ValidateClaimSignature(cert_claim, claim_addr, cert_id, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != false {
|
||||
t.Error("failed to validate signature:", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2ValidateClaimSignature(t *testing.T) {
|
||||
cert_claim_hex := "00125a0a583056301006072a8648ce3d020106052b8104000a034200045a0343c155302280da01ae0001b7295241eb03c42a837acf92ccb9680892f7db50fd1d3c14b28bb594e304f05fc4ae7c1f222a85d1d1a3461b3cfb9906f66cb5"
|
||||
signed_claim_hex := "015cb78e424a34fbf79b67f9107430427aa62373e69b4998a29ecec8f14a9e0a213a043ced8064c069d7e464b5fd3ccb92b45bd59b15c0e1bb27e3c366d43f86a9a6b5ad42647a1aad69a73ac50b19ae3ec978c2c70aa2010a99010a301c662f19abc461e7eddecf165adfa7fca569e209773f3db31241c1e297f0a8d5b3e4768828b065fbeb1d6776f61073f6121b3031202d20556e6d6173746572656420496d70756c7365732e377a187a22146170706c69636174696f6e2f782d6578742d377a32302eb61ea475017e28c013616a56c1219ba90dc35fffff453d9675146f648f66634e0d1516528d37aba9f5801229d9f2181a044e6f6e6542087465737420707562520062020801"
|
||||
|
||||
signed_claim, err := DecodeClaimHex(signed_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cert_claim, err := DecodeClaimHex(cert_claim_hex, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
firstInputTxHash, err := GetOutpointHash("becb96a4a2e66bd24f083772fe9da904654ea9b5f07cc5bfbee233355911ddb1", uint32(0))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cert_id := "e67323a67a42307410f9679bf7fb344a428eb75c"
|
||||
|
||||
result, err := signed_claim.ValidateClaimSignature(cert_claim, firstInputTxHash, cert_id, "lbrycrd_main")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != true {
|
||||
t.Error("failed to validate signature:", result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetOutpointHash(t *testing.T) {
|
||||
hash, err := GetOutpointHash("dc3dcf2f94d3c91e454ac2474802e20f26b30705372dda43890c811d918aef64", 1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Assert(t, hash == "64ef8a911d810c8943da2d370507b3260fe2024847c24a451ec9d3942fcf3ddc01000000", uint(1))
|
||||
}
|
1
schema/test.sh
Executable file
1
schema/test.sh
Executable file
|
@ -0,0 +1 @@
|
|||
go test ./... -v
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
export IMPORTPATH="github.com/lbryio/lbry.go"
|
||||
export IMPORTPATH="github.com/lbryio/lbry.go/v2"
|
||||
export VERSIONSHORT="${TRAVIS_COMMIT:-"$(git describe --tags --always --dirty)"}"
|
||||
export VERSIONLONG="${TRAVIS_COMMIT:-"$(git describe --tags --always --dirty --long)"}"
|
||||
export COMMITMSG="$(echo ${TRAVIS_COMMIT_MESSAGE:-"$(git show -s --format=%s)"} | tr -d '"' | head -n 1)"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
GO111MODULE=off go get github.com/caarlos0/svu
|
||||
go install github.com/caarlos0/svu@latest
|
||||
git tag `svu "$1"`
|
||||
git push --tags
|
||||
|
|
|
@ -8,10 +8,14 @@ import (
|
|||
"encoding/hex"
|
||||
"strconv"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
const MaxBlobSize = 2097152 // 2mb, or 2 * 2^20
|
||||
const (
|
||||
MaxBlobSize = 2097152 // 2mb, or 2 * 2^20
|
||||
BlobHashSize = sha512.Size384
|
||||
BlobHashHexLength = BlobHashSize * 2 // in hex, each byte is 2 chars
|
||||
)
|
||||
|
||||
type Blob []byte
|
||||
|
||||
|
@ -31,7 +35,7 @@ func (b Blob) Hash() []byte {
|
|||
return hashBytes[:]
|
||||
}
|
||||
|
||||
// HashHex returns th blob hash as a hex string
|
||||
// HashHex returns the blob hash as a hex string
|
||||
func (b Blob) HashHex() string {
|
||||
return hex.EncodeToString(b.Hash())
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
// inspired by https://blog.gopheracademy.com/advent-2016/advanced-encoding-decoding/
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const streamTypeLBRYFile = "lbryfile"
|
||||
|
@ -44,10 +45,39 @@ type SDBlob struct {
|
|||
StreamHash []byte `json:"-"`
|
||||
}
|
||||
|
||||
// Hash returns a hash of the SD blob data
|
||||
func (s SDBlob) Hash() []byte {
|
||||
hashBytes := sha512.Sum384(s.ToBlob())
|
||||
return hashBytes[:]
|
||||
}
|
||||
|
||||
// HashHex returns the SD blob hash as a hex string
|
||||
func (s SDBlob) HashHex() string {
|
||||
return hex.EncodeToString(s.Hash())
|
||||
}
|
||||
|
||||
// ToJson returns the SD blob hash as JSON
|
||||
func (s SDBlob) ToJson() string {
|
||||
j, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}
|
||||
|
||||
// ToBlob converts the SDBlob to a normal data Blob
|
||||
func (s SDBlob) ToBlob() (Blob, error) {
|
||||
b, err := json.Marshal(s)
|
||||
return Blob(b), err
|
||||
func (s SDBlob) ToBlob() Blob {
|
||||
jsonSD, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// COMPATIBILITY HACK to make json output match python's json. this can be
|
||||
// removed when we implement canonical JSON encoding
|
||||
jsonSD = []byte(strings.Replace(string(jsonSD), ",", ", ", -1))
|
||||
jsonSD = []byte(strings.Replace(string(jsonSD), ":", ": ", -1))
|
||||
|
||||
return jsonSD
|
||||
}
|
||||
|
||||
// FromBlob unmarshals a data Blob that should contain SDBlob data
|
||||
|
@ -55,30 +85,6 @@ func (s *SDBlob) FromBlob(b Blob) error {
|
|||
return json.Unmarshal(b, s)
|
||||
}
|
||||
|
||||
func newSdBlob(blobs []Blob, key []byte, ivs [][]byte, streamName, suggestedFilename string) *SDBlob {
|
||||
if len(ivs) != len(blobs)+1 { // +1 for terminating 0-length blob
|
||||
panic("wrong number of IVs provided")
|
||||
}
|
||||
|
||||
sd := &SDBlob{
|
||||
StreamType: streamTypeLBRYFile,
|
||||
StreamName: streamName,
|
||||
SuggestedFileName: suggestedFilename,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
for i, b := range blobs {
|
||||
sd.addBlob(b, ivs[i])
|
||||
}
|
||||
|
||||
// terminating blob
|
||||
sd.addBlob(Blob{}, ivs[len(ivs)-1])
|
||||
|
||||
sd.updateStreamHash()
|
||||
|
||||
return sd
|
||||
}
|
||||
|
||||
// addBlob adds the blob's info to stream
|
||||
func (s *SDBlob) addBlob(b Blob, iv []byte) {
|
||||
if len(iv) == 0 {
|
||||
|
|
236
stream/stream.go
236
stream/stream.go
|
@ -2,10 +2,12 @@ package stream
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/extras/errors"
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
type Stream []Blob
|
||||
|
@ -13,66 +15,22 @@ type Stream []Blob
|
|||
// -1 to leave room for padding, since there must be at least one byte of pkcs7 padding
|
||||
const maxBlobDataSize = MaxBlobSize - 1
|
||||
|
||||
// New creates a new Stream from a byte slice
|
||||
func New(data []byte) (Stream, error) {
|
||||
key := randIV()
|
||||
ivs := make([][]byte, numContentBlobs(data)+1) // +1 for terminating 0-length blob
|
||||
for i := range ivs {
|
||||
ivs[i] = randIV()
|
||||
}
|
||||
|
||||
return makeStream(data, key, ivs, "", "")
|
||||
}
|
||||
|
||||
// Reconstruct creates a stream from the given data using predetermined IVs and key from the SD blob
|
||||
// NOTE: this will assume that all blobs except the last one are at max length. in theory this is not
|
||||
// required, but in practice this is always true. if this is false, streams may not match exactly
|
||||
func Reconstruct(data []byte, sdBlob SDBlob) (Stream, error) {
|
||||
ivs := make([][]byte, len(sdBlob.BlobInfos))
|
||||
for i := range ivs {
|
||||
ivs[i] = sdBlob.BlobInfos[i].IV
|
||||
}
|
||||
|
||||
return makeStream(data, sdBlob.Key, ivs, sdBlob.StreamName, sdBlob.SuggestedFileName)
|
||||
}
|
||||
|
||||
func makeStream(data, key []byte, ivs [][]byte, streamName, suggestedFilename string) (Stream, error) {
|
||||
var err error
|
||||
|
||||
numBlobs := numContentBlobs(data)
|
||||
if len(ivs) != numBlobs+1 { // +1 for terminating 0-length blob
|
||||
return nil, errors.Err("incorrect number of IVs provided")
|
||||
}
|
||||
|
||||
s := make(Stream, numBlobs+1) // +1 for sd blob
|
||||
for i := 0; i < numBlobs; i++ {
|
||||
start := i * maxBlobDataSize
|
||||
end := start + maxBlobDataSize
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
s[i+1], err = NewBlob(data[start:end], key, ivs[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sd := newSdBlob(s[1:], key, ivs, streamName, suggestedFilename)
|
||||
jsonSD, err := sd.ToBlob()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// COMPATIBILITY HACK to make json output match python's json. this can be
|
||||
// removed when we implement canonical JSON encoding
|
||||
jsonSD = []byte(strings.Replace(string(jsonSD), ",", ", ", -1))
|
||||
jsonSD = []byte(strings.Replace(string(jsonSD), ":", ": ", -1))
|
||||
|
||||
s[0] = jsonSD
|
||||
return s, nil
|
||||
// New creates a new Stream from a stream of bytes.
|
||||
func New(src io.Reader) (Stream, error) {
|
||||
return NewEncoder(src).Stream()
|
||||
}
|
||||
|
||||
// Data returns the file data that a stream encapsulates.
|
||||
//
|
||||
// Deprecated: use Decode() instead. It's a more accurate name. Data() will be removed in the future.
|
||||
func (s Stream) Data() ([]byte, error) {
|
||||
return s.Decode()
|
||||
}
|
||||
|
||||
// Decode returns the file data that a stream encapsulates
|
||||
//
|
||||
// TODO: this should use io.Writer instead of returning bytes
|
||||
func (s Stream) Decode() ([]byte, error) {
|
||||
if len(s) < 2 {
|
||||
return nil, errors.Err("stream must be at least 2 blobs long") // sd blob and content blob
|
||||
}
|
||||
|
@ -124,7 +82,161 @@ func (s Stream) Data() ([]byte, error) {
|
|||
return file, nil
|
||||
}
|
||||
|
||||
//numContentBlobs returns the number of content blobs required to store the data
|
||||
func numContentBlobs(data []byte) int {
|
||||
return int(math.Ceil(float64(len(data)) / float64(maxBlobDataSize)))
|
||||
// Encoder reads bytes from a source and returns blobs of the stream
|
||||
type Encoder struct {
|
||||
// source data to be encoded into a stream
|
||||
src io.Reader
|
||||
// preset IVs to use for encrypting blobs
|
||||
ivs [][]byte
|
||||
// an optionals hint about the total size of the source data
|
||||
// encoder will use this to preallocate space for blobs
|
||||
srcSizeHint int
|
||||
|
||||
// buffer for reading bytes from reader
|
||||
buf []byte
|
||||
// sd blob that gets built as stream is encoded
|
||||
sd *SDBlob
|
||||
// number of bytes read from src
|
||||
srcLen int
|
||||
// running hash bytes read from src
|
||||
srcHash hash.Hash
|
||||
}
|
||||
|
||||
// NewEncoder creates a new stream encoder
|
||||
func NewEncoder(src io.Reader) *Encoder {
|
||||
return &Encoder{
|
||||
src: src,
|
||||
|
||||
buf: make([]byte, maxBlobDataSize),
|
||||
sd: &SDBlob{
|
||||
StreamType: streamTypeLBRYFile,
|
||||
Key: randIV(),
|
||||
},
|
||||
srcHash: sha512.New384(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewEncoderWithIVs creates a new encoder that uses preset cryptographic material
|
||||
func NewEncoderWithIVs(src io.Reader, key []byte, ivs [][]byte) *Encoder {
|
||||
e := NewEncoder(src)
|
||||
e.sd.Key = key
|
||||
e.ivs = ivs
|
||||
return e
|
||||
}
|
||||
|
||||
// NewEncoderFromSD creates a new encoder that reuses cryptographic material from an sd blob
|
||||
// This can be used to reconstruct a stream exactly from a file
|
||||
// NOTE: this will assume that all blobs except the last one are at max length. in theory this is not
|
||||
// required, but in practice this is always true. if this is false, streams may not match exactly
|
||||
func NewEncoderFromSD(src io.Reader, sdBlob *SDBlob) *Encoder {
|
||||
ivs := make([][]byte, len(sdBlob.BlobInfos))
|
||||
for i := range ivs {
|
||||
ivs[i] = sdBlob.BlobInfos[i].IV
|
||||
}
|
||||
|
||||
e := NewEncoderWithIVs(src, sdBlob.Key, ivs)
|
||||
e.sd.StreamName = sdBlob.StreamName
|
||||
e.sd.SuggestedFileName = sdBlob.SuggestedFileName
|
||||
return e
|
||||
}
|
||||
|
||||
// TODO: consider making a NewPartialEncoder that also copies blobinfos from sdBlobs and seeks forward in the data
|
||||
// this would avoid re-creating blobs that were created in the past
|
||||
|
||||
// Next reads the next chunk of data, encodes it into a blob, and adds it to the stream
|
||||
// When the source is fully consumed, Next() makes sure the stream is terminated (i.e. the sd blob
|
||||
// ends with an empty terminating blob) and returns io.EOF
|
||||
func (e *Encoder) Next() (Blob, error) {
|
||||
n, err := e.src.Read(e.buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
e.ensureTerminated()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.srcLen += n
|
||||
e.srcHash.Write(e.buf[:n])
|
||||
iv := e.nextIV()
|
||||
|
||||
blob, err := NewBlob(e.buf[:n], e.sd.Key, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.sd.addBlob(blob, iv)
|
||||
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
// Stream creates the whole stream in one call
|
||||
func (e *Encoder) Stream() (Stream, error) {
|
||||
s := make(Stream, 1, 1+int(math.Ceil(float64(e.srcSizeHint)/maxBlobDataSize))) // len starts at 1 and cap is +1 to leave room for sd blob
|
||||
|
||||
for {
|
||||
blob, err := e.Next()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = append(s, blob)
|
||||
}
|
||||
|
||||
s[0] = e.SDBlob().ToBlob()
|
||||
|
||||
if cap(s) > len(s) {
|
||||
// size hint was too big. copy stream to smaller underlying array to free memory
|
||||
// this might be premature optimization...
|
||||
s = append(Stream(nil), s[:]...)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// SDBlob returns the sd blob so far
|
||||
func (e *Encoder) SDBlob() *SDBlob {
|
||||
e.sd.updateStreamHash()
|
||||
return e.sd
|
||||
}
|
||||
|
||||
// SourceLen returns the number of bytes read from source
|
||||
func (e *Encoder) SourceLen() int {
|
||||
return e.srcLen
|
||||
}
|
||||
|
||||
// SourceLen returns a hash of the bytes read from source
|
||||
func (e *Encoder) SourceHash() []byte {
|
||||
return e.srcHash.Sum(nil)
|
||||
}
|
||||
|
||||
// SourceSizeHint sets a hint about the total size of the source
|
||||
// This helps allocate RAM more efficiently.
|
||||
// If the hint is wrong, it still works fine but there will be a small performance penalty.
|
||||
func (e *Encoder) SourceSizeHint(size int) *Encoder {
|
||||
e.srcSizeHint = size
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Encoder) isTerminated() bool {
|
||||
return len(e.sd.BlobInfos) >= 1 && e.sd.BlobInfos[len(e.sd.BlobInfos)-1].Length == 0
|
||||
}
|
||||
|
||||
func (e *Encoder) ensureTerminated() {
|
||||
if !e.isTerminated() {
|
||||
e.sd.addBlob(Blob{}, e.nextIV())
|
||||
}
|
||||
}
|
||||
|
||||
// nextIV returns the next preset IV if there is one
|
||||
func (e *Encoder) nextIV() []byte {
|
||||
if len(e.ivs) == 0 {
|
||||
return randIV()
|
||||
}
|
||||
|
||||
iv := e.ivs[0]
|
||||
e.ivs = e.ivs[1:]
|
||||
return iv
|
||||
}
|
||||
|
|
|
@ -2,26 +2,31 @@ package stream
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||
)
|
||||
|
||||
func TestStreamToFile(t *testing.T) {
|
||||
blobHashes := []string{
|
||||
var testdataBlobHashes = []string{
|
||||
"1bf7d39c45d1a38ffa74bff179bf7f67d400ff57fa0b5a0308963f08d01712b3079530a8c188e8c89d9b390c6ee06f05", // sd hash
|
||||
"a2f1841bb9c5f3b583ac3b8c07ee1a5bf9cc48923721c30d5ca6318615776c284e8936d72fa4db7fdda2e4e9598b1e6c",
|
||||
"0c9675ad7f40f29dcd41883ed9cf7e145bbb13976d9b83ab9354f4f61a87f0f7771a56724c2aa7a5ab43c68d7942e5cb",
|
||||
"a4d07d442b9907036c75b6c92db316a8b8428733bf5ec976627a48a7c862bf84db33075d54125a7c0b297bd2dc445f1c",
|
||||
"dcd2093f4a3eca9f6dd59d785d0bef068fee788481986aa894cf72ed4d992c0ff9d19d1743525de2f5c3c62f5ede1c58",
|
||||
}
|
||||
}
|
||||
|
||||
stream := make(Stream, len(blobHashes))
|
||||
for i, hash := range blobHashes {
|
||||
func TestStreamToFile(t *testing.T) {
|
||||
stream := make(Stream, len(testdataBlobHashes))
|
||||
for i, hash := range testdataBlobHashes {
|
||||
stream[i] = testdata(t, hash)
|
||||
}
|
||||
|
||||
data, err := stream.Data()
|
||||
data, err := stream.Decode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -33,6 +38,8 @@ func TestStreamToFile(t *testing.T) {
|
|||
t.Errorf("file length mismatch. got %d, expected %d", actualLen, expectedLen)
|
||||
}
|
||||
|
||||
expectedFileHash := sha512.Sum384(data)
|
||||
|
||||
expectedSha256 := unhex(t, "51e4d03bd6d69ea17d1be3ce01fdffa44ffe053f2dbce8d42a50283b2890fea2")
|
||||
actualSha256 := sha256.Sum256(data)
|
||||
|
||||
|
@ -46,22 +53,150 @@ func TestStreamToFile(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newStream, err := Reconstruct(data, *sdBlob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
enc := NewEncoderFromSD(bytes.NewBuffer(data), sdBlob)
|
||||
newStream, err := enc.Stream()
|
||||
|
||||
if len(newStream) != len(testdataBlobHashes) {
|
||||
t.Fatalf("stream length mismatch. got %d blobs, expected %d", len(newStream), len(testdataBlobHashes))
|
||||
}
|
||||
|
||||
if len(newStream) != len(blobHashes) {
|
||||
t.Fatalf("stream length mismatch. got %d blobs, expected %d", len(newStream), len(blobHashes))
|
||||
if enc.SourceLen() != expectedLen {
|
||||
t.Errorf("reconstructed file length mismatch. got %d, expected %d", enc.SourceLen(), expectedLen)
|
||||
}
|
||||
|
||||
for i, hash := range blobHashes {
|
||||
if !bytes.Equal(enc.SourceHash(), expectedFileHash[:]) {
|
||||
t.Errorf("reconstructed file hash mismatch. got %s, expected %s", hex.EncodeToString(enc.SourceHash()), hex.EncodeToString(expectedFileHash[:]))
|
||||
}
|
||||
|
||||
for i, hash := range testdataBlobHashes {
|
||||
if newStream[i].HashHex() != hash {
|
||||
t.Errorf("blob %d hash mismatch. got %s, expected %s", i, newStream[i].HashHex(), hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeStream(t *testing.T) {
|
||||
blobsToRead := 3
|
||||
totalBlobs := blobsToRead + 3
|
||||
|
||||
data := make([]byte, ((totalBlobs-1)*maxBlobDataSize)+1000) // last blob is partial
|
||||
_, err := rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
|
||||
enc := NewEncoder(buf)
|
||||
|
||||
stream := make(Stream, blobsToRead+1) // +1 for sd blob
|
||||
for i := 1; i < blobsToRead+1; i++ { // start at 1 to skip sd blob
|
||||
stream[i], err = enc.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
sdBlob := enc.SDBlob()
|
||||
|
||||
if len(sdBlob.BlobInfos) != blobsToRead {
|
||||
t.Errorf("expected %d blobs in partial sdblob, got %d", blobsToRead, len(sdBlob.BlobInfos))
|
||||
}
|
||||
if enc.SourceLen() != maxBlobDataSize*blobsToRead {
|
||||
t.Errorf("expected length of %d , got %d", maxBlobDataSize*blobsToRead, enc.SourceLen())
|
||||
}
|
||||
|
||||
// now finish the stream, reusing key and IVs
|
||||
|
||||
buf = bytes.NewBuffer(data) // rewind to the beginning of the data
|
||||
|
||||
enc = NewEncoderFromSD(buf, sdBlob)
|
||||
|
||||
reconstructedStream, err := enc.Stream()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(reconstructedStream) != totalBlobs+1 { // +1 for the terminating blob at the end
|
||||
t.Errorf("expected %d blobs in stream, got %d", totalBlobs+1, len(reconstructedStream))
|
||||
}
|
||||
if enc.SourceLen() != len(data) {
|
||||
t.Errorf("expected length of %d , got %d", len(data), enc.SourceLen())
|
||||
}
|
||||
|
||||
reconstructedSDBlob := enc.SDBlob()
|
||||
|
||||
for i := 0; i < len(sdBlob.BlobInfos); i++ {
|
||||
if !bytes.Equal(sdBlob.BlobInfos[i].IV, reconstructedSDBlob.BlobInfos[i].IV) {
|
||||
t.Errorf("blob info %d of reconstructed sd blobd does not match original sd blob", i)
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(stream); i++ { // start at 1 to skip sd blob
|
||||
if !bytes.Equal(stream[i], reconstructedStream[i]) {
|
||||
t.Errorf("blob %d of reconstructed stream does not match original stream", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyStream(t *testing.T) {
|
||||
enc := NewEncoder(bytes.NewBuffer(nil))
|
||||
_, err := enc.Next()
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Errorf("expected io.EOF, got %v", err)
|
||||
}
|
||||
sd := enc.SDBlob()
|
||||
if len(sd.BlobInfos) != 1 {
|
||||
t.Errorf("expected 1 blobinfos in sd blob, got %d", len(sd.BlobInfos))
|
||||
}
|
||||
if sd.BlobInfos[0].Length != 0 {
|
||||
t.Errorf("first and only blob to be the terminator blob")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTermination(t *testing.T) {
|
||||
b := make([]byte, 12)
|
||||
|
||||
enc := NewEncoder(bytes.NewBuffer(b))
|
||||
|
||||
_, err := enc.Next()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if enc.isTerminated() {
|
||||
t.Errorf("stream should not terminate until after EOF")
|
||||
}
|
||||
|
||||
_, err = enc.Next()
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Errorf("expected io.EOF, got %v", err)
|
||||
}
|
||||
if !enc.isTerminated() {
|
||||
t.Errorf("stream should be terminated after EOF")
|
||||
}
|
||||
|
||||
_, err = enc.Next()
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Errorf("expected io.EOF on all subsequent reads, got %v", err)
|
||||
}
|
||||
sd := enc.SDBlob()
|
||||
if len(sd.BlobInfos) != 2 {
|
||||
t.Errorf("expected 2 blobinfos in sd blob, got %d", len(sd.BlobInfos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSizeHint(t *testing.T) {
|
||||
b := make([]byte, 12)
|
||||
|
||||
newStream, err := NewEncoder(bytes.NewBuffer(b)).SourceSizeHint(5 * maxBlobDataSize).Stream()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cap(newStream) != 2 { // 1 for sd blob, 1 for the 12 bytes of the actual stream
|
||||
t.Fatalf("expected 2 blobs allocated, got %d", cap(newStream))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Skip("TODO: test new stream creation and decryption")
|
||||
}
|
||||
|
|
347
url/url.go
Normal file
347
url/url.go
Normal file
|
@ -0,0 +1,347 @@
|
|||
package url
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const regexPartProtocol = "^((?:lbry://|https://)?)"
|
||||
const regexPartHost = "((?:open.lbry.com/|lbry.tv/|lbry.lat/|lbry.fr/|lbry.in/)?)"
|
||||
const regexPartStreamOrChannelName = "([^:$#/]*)"
|
||||
const regexPartModifierSeparator = "([:$#]?)([^/]*)"
|
||||
const regexQueryStringBreaker = "^([\\S]+)([?][\\S]*)"
|
||||
const urlComponentsSize = 9
|
||||
|
||||
const ChannelNameMinLength = 1
|
||||
const ClaimIdMaxLength = 40
|
||||
const ProtoDefault = "lbry://"
|
||||
const RegexClaimId = "(?i)^[0-9a-f]+$"
|
||||
const RegexInvalidUri = "(?i)[ =&#:$@%?;/\\\\\\\\\\\"<>%\\\\{\\\\}|^~\\\\[\\\\]`\\u0000-\\u0008\\u000b-\\u000c\\u000e-\\u001F\\uD800-\\uDFFF\\uFFFE-\\uFFFF]"
|
||||
|
||||
type LbryUri struct {
|
||||
Path string
|
||||
IsChannel bool
|
||||
StreamName string
|
||||
StreamClaimId string
|
||||
ChannelName string
|
||||
ChannelClaimId string
|
||||
PrimaryClaimSequence int
|
||||
SecondaryClaimSequence int
|
||||
PrimaryBidPosition int
|
||||
SecondaryBidPosition int
|
||||
ClaimName string
|
||||
ClaimId string
|
||||
ContentName string
|
||||
QueryString string
|
||||
}
|
||||
|
||||
type UriModifier struct {
|
||||
ClaimId string
|
||||
ClaimSequence int
|
||||
BidPosition int
|
||||
}
|
||||
|
||||
func (uri LbryUri) IsChannelUrl() bool {
|
||||
return (!isEmpty(uri.ChannelName) && isEmpty(uri.StreamName)) || (!isEmpty(uri.ClaimName) && strings.HasPrefix(uri.ClaimName, "@"))
|
||||
}
|
||||
|
||||
func (uri LbryUri) IsNameValid(name string) bool {
|
||||
return !regexp.MustCompile(RegexInvalidUri).MatchString(name)
|
||||
}
|
||||
|
||||
func (uri LbryUri) String() string {
|
||||
return uri.Build(true, ProtoDefault, false)
|
||||
}
|
||||
|
||||
func (uri LbryUri) VanityString() string {
|
||||
return uri.Build(true, ProtoDefault, true)
|
||||
}
|
||||
|
||||
func (uri LbryUri) TvString() string {
|
||||
return uri.Build(true, "https://lbry.tv/", false)
|
||||
}
|
||||
|
||||
func (uri LbryUri) Build(includeProto bool, protocol string, vanity bool) string {
|
||||
formattedChannelName := ""
|
||||
if !isEmpty(uri.ChannelName) {
|
||||
formattedChannelName = uri.ChannelName
|
||||
if !strings.HasPrefix(formattedChannelName, "@") {
|
||||
formattedChannelName = fmt.Sprintf("@%s", formattedChannelName)
|
||||
}
|
||||
}
|
||||
primaryClaimName := uri.ClaimName
|
||||
if isEmpty(primaryClaimName) {
|
||||
primaryClaimName = uri.ContentName
|
||||
}
|
||||
if isEmpty(primaryClaimName) {
|
||||
primaryClaimName = formattedChannelName
|
||||
}
|
||||
if isEmpty(primaryClaimName) {
|
||||
primaryClaimName = uri.StreamName
|
||||
}
|
||||
|
||||
primaryClaimId := uri.ClaimId
|
||||
if isEmpty(primaryClaimId) {
|
||||
if !isEmpty(formattedChannelName) {
|
||||
primaryClaimId = uri.ChannelClaimId
|
||||
} else {
|
||||
primaryClaimId = uri.StreamClaimId
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if includeProto {
|
||||
sb.WriteString(protocol)
|
||||
}
|
||||
sb.WriteString(primaryClaimName)
|
||||
if vanity {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
secondaryClaimName := ""
|
||||
if isEmpty(uri.ClaimName) && !isEmpty(uri.ContentName) {
|
||||
secondaryClaimName = uri.ContentName
|
||||
}
|
||||
if isEmpty(secondaryClaimName) {
|
||||
if !isEmpty(formattedChannelName) {
|
||||
secondaryClaimName = uri.StreamName
|
||||
}
|
||||
}
|
||||
secondaryClaimId := ""
|
||||
if !isEmpty(secondaryClaimName) {
|
||||
secondaryClaimId = uri.StreamClaimId
|
||||
}
|
||||
|
||||
if !isEmpty(primaryClaimId) {
|
||||
sb.WriteString("#")
|
||||
sb.WriteString(primaryClaimId)
|
||||
} else if uri.PrimaryClaimSequence > 0 {
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(strconv.Itoa(uri.PrimaryClaimSequence))
|
||||
} else if uri.PrimaryBidPosition > 0 {
|
||||
sb.WriteString("$")
|
||||
sb.WriteString(strconv.Itoa(uri.PrimaryBidPosition))
|
||||
}
|
||||
|
||||
if !isEmpty(secondaryClaimName) {
|
||||
sb.WriteString("/")
|
||||
sb.WriteString(secondaryClaimName)
|
||||
}
|
||||
|
||||
if !isEmpty(secondaryClaimId) {
|
||||
sb.WriteString("#")
|
||||
sb.WriteString(secondaryClaimId)
|
||||
} else if uri.SecondaryClaimSequence > 0 {
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(strconv.Itoa(uri.SecondaryClaimSequence))
|
||||
} else if uri.SecondaryBidPosition > 0 {
|
||||
sb.WriteString("$")
|
||||
sb.WriteString(strconv.Itoa(uri.SecondaryBidPosition))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func Parse(url string, requireProto bool) (*LbryUri, error) {
|
||||
if isEmpty(url) {
|
||||
return nil, errors.New("invalid url parameter")
|
||||
}
|
||||
|
||||
reComponents := regexp.MustCompile(
|
||||
fmt.Sprintf("(?i)%s%s%s%s(/?)%s%s",
|
||||
regexPartProtocol,
|
||||
regexPartHost,
|
||||
regexPartStreamOrChannelName,
|
||||
regexPartModifierSeparator,
|
||||
regexPartStreamOrChannelName,
|
||||
regexPartModifierSeparator))
|
||||
reSeparateQueryString := regexp.MustCompile(regexQueryStringBreaker)
|
||||
|
||||
cleanUrl := url
|
||||
queryString := ""
|
||||
|
||||
qsMatches := reSeparateQueryString.FindStringSubmatch(url)
|
||||
if len(qsMatches) == 3 {
|
||||
cleanUrl = qsMatches[1]
|
||||
queryString = qsMatches[2][1:]
|
||||
}
|
||||
|
||||
var components []string
|
||||
componentMatches := reComponents.FindStringSubmatch(cleanUrl)
|
||||
for _, component := range componentMatches[1:] {
|
||||
components = append(components, component)
|
||||
}
|
||||
if len(components) != urlComponentsSize {
|
||||
return nil, errors.New("regular expression error occurred while trying to Parse the value")
|
||||
}
|
||||
|
||||
/*
|
||||
* components[0] = proto
|
||||
* components[1] = host
|
||||
* components[2] = streamName or channelName
|
||||
* components[3] = primaryModSeparator
|
||||
* components[4] = primaryModValue
|
||||
* components[5] = path separator
|
||||
* components[6] = possibleStreamName
|
||||
* components[7] = secondaryModSeparator
|
||||
* components[8] = secondaryModValue
|
||||
*/
|
||||
if requireProto && isEmpty(components[0]) {
|
||||
return nil, errors.New("url must include a protocol prefix (lbry://)")
|
||||
}
|
||||
if isEmpty(components[2]) {
|
||||
return nil, errors.New("url does not include a name")
|
||||
}
|
||||
for _, component := range components[2:] {
|
||||
if strings.Index(component, " ") > -1 {
|
||||
return nil, errors.New("url cannot include a space")
|
||||
}
|
||||
}
|
||||
|
||||
streamOrChannelName := components[2]
|
||||
primaryModSeparator := components[3]
|
||||
primaryModValue := components[4]
|
||||
possibleStreamName := components[6]
|
||||
secondaryModSeparator := components[7]
|
||||
secondaryModValue := components[8]
|
||||
primaryClaimId := ""
|
||||
primaryClaimSequence := -1
|
||||
primaryBidPosition := -1
|
||||
secondaryClaimSequence := -1
|
||||
secondaryBidPosition := -1
|
||||
|
||||
includesChannel := strings.HasPrefix(streamOrChannelName, "@")
|
||||
isChannel := includesChannel && isEmpty(possibleStreamName)
|
||||
channelName := ""
|
||||
if includesChannel && len(streamOrChannelName) > 1 {
|
||||
channelName = streamOrChannelName[1:]
|
||||
}
|
||||
|
||||
// Convert the mod separators when parsing with protocol https://lbry.tv/ or similar
|
||||
// [https://] uses ':', [lbry://] expects #
|
||||
if !isEmpty(components[1]) {
|
||||
if primaryModSeparator == ":" {
|
||||
primaryModSeparator = "#"
|
||||
}
|
||||
if secondaryModSeparator == ":" {
|
||||
secondaryModSeparator = "#"
|
||||
}
|
||||
}
|
||||
|
||||
if includesChannel {
|
||||
if isEmpty(channelName) {
|
||||
// I wonder if this check is really necessary, considering the subsequent min length check
|
||||
return nil, errors.New("no channel name after @")
|
||||
}
|
||||
if len(channelName) < ChannelNameMinLength {
|
||||
return nil, errors.New(fmt.Sprintf("Channel names must be at least %d character long.", ChannelNameMinLength))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var primaryMod *UriModifier
|
||||
var secondaryMod *UriModifier
|
||||
if !isEmpty(primaryModSeparator) && !isEmpty(primaryModValue) {
|
||||
primaryMod, err = parseModifier(primaryModSeparator, primaryModValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primaryClaimId = primaryMod.ClaimId
|
||||
primaryClaimSequence = primaryMod.ClaimSequence
|
||||
primaryBidPosition = primaryMod.BidPosition
|
||||
}
|
||||
if !isEmpty(secondaryModSeparator) && !isEmpty(secondaryModValue) {
|
||||
secondaryMod, err = parseModifier(secondaryModSeparator, secondaryModValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secondaryClaimSequence = secondaryMod.ClaimSequence
|
||||
secondaryBidPosition = secondaryMod.BidPosition
|
||||
}
|
||||
|
||||
streamName := streamOrChannelName
|
||||
if includesChannel {
|
||||
streamName = possibleStreamName
|
||||
}
|
||||
|
||||
streamClaimId := ""
|
||||
if includesChannel && secondaryMod != nil {
|
||||
streamClaimId = secondaryMod.ClaimId
|
||||
} else if primaryMod != nil {
|
||||
streamClaimId = primaryMod.ClaimId
|
||||
}
|
||||
channelClaimId := ""
|
||||
if includesChannel && primaryMod != nil {
|
||||
channelClaimId = primaryMod.ClaimId
|
||||
}
|
||||
|
||||
return &LbryUri{
|
||||
Path: strings.Join(components[2:], ""),
|
||||
IsChannel: isChannel,
|
||||
StreamName: streamName,
|
||||
StreamClaimId: streamClaimId,
|
||||
ChannelName: channelName,
|
||||
ChannelClaimId: channelClaimId,
|
||||
PrimaryClaimSequence: primaryClaimSequence,
|
||||
SecondaryClaimSequence: secondaryClaimSequence,
|
||||
PrimaryBidPosition: primaryBidPosition,
|
||||
SecondaryBidPosition: secondaryBidPosition,
|
||||
ClaimName: streamOrChannelName,
|
||||
ClaimId: primaryClaimId,
|
||||
ContentName: streamName,
|
||||
QueryString: queryString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseModifier(modSeparator string, modValue string) (*UriModifier, error) {
|
||||
claimId := ""
|
||||
claimSequence := 0
|
||||
bidPosition := 0
|
||||
|
||||
if !isEmpty(modSeparator) {
|
||||
if isEmpty(modValue) {
|
||||
return nil, errors.New(fmt.Sprintf("No modifier provided after separator %s", modSeparator))
|
||||
}
|
||||
|
||||
if modSeparator == "#" {
|
||||
claimId = modValue
|
||||
} else if modSeparator == ":" {
|
||||
claimId = modValue
|
||||
} else if modSeparator == "$" {
|
||||
bidPosition = parseInt(modValue, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if !isEmpty(claimId) && (len(claimId) > ClaimIdMaxLength || !regexp.MustCompile(RegexClaimId).MatchString(claimId)) {
|
||||
return nil, errors.New(fmt.Sprintf("Invalid claim ID %s", claimId))
|
||||
}
|
||||
if claimSequence == -1 {
|
||||
return nil, errors.New("claim sequence must be a number")
|
||||
}
|
||||
if bidPosition == -1 {
|
||||
return nil, errors.New("bid position must be a number")
|
||||
}
|
||||
|
||||
return &UriModifier{
|
||||
ClaimId: claimId,
|
||||
ClaimSequence: claimSequence,
|
||||
BidPosition: bidPosition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseInt(value string, defaultValue int) int {
|
||||
v, err := strconv.ParseInt(value, 10, 32)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return int(v)
|
||||
}
|
||||
|
||||
func isEmpty(s string) bool {
|
||||
return len(strings.TrimSpace(s)) == 0
|
||||
}
|
Loading…
Reference in a new issue