Compare commits

..

233 commits

Author SHA1 Message Date
Niko Storni
a01aa6dc06 upgrade dependencies 2023-03-07 19:14:31 +01:00
Niko Storni
e6a3f40029 Merge branch 'scheduled-unlisted' 2022-11-01 22:28:32 +01:00
Niko Storni
ced09b22ca convert type to variadic 2022-11-01 22:28:18 +01:00
Thomas Zarebczan
fa55e82bc1
feat: add scheduled and unlisted 2022-11-01 16:59:07 -04:00
Niko Storni
77944ba3af fix nil ptr 2022-09-27 22:56:34 +02:00
Niko Storni
5f52a995a7 retry failed messages 2022-09-27 22:48:51 +02:00
Niko Storni
014adbb315 actually use the new client 2022-09-27 21:35:13 +02:00
Niko Storni
73228d1bfb use custom http client for slack messages 2022-09-27 20:21:02 +02:00
Niko Storni
69cfd7f798 add account_send
adjust transaction summary fields
2022-09-21 22:53:02 +02:00
Niko Storni
41555cbda2 add helper for special claim tags 2022-09-16 03:18:01 +02:00
Niko Storni
2adb8af5b6 add funding accounts to channel create options 2022-08-15 22:41:00 +02:00
Mark Beamer Jr
9130630afe
Added additional functionality to allow for both objects and arrays to be returned from internal-apis client.
Also added a raw API url call and converted current call to a call to a resource so we are not restricted to that format to use the library.
2022-08-05 13:51:05 -04:00
Alex Grin
8e6d493fbf
Merge pull request #93 from andybeletsky/master
Add metadata to error returned by jsonrpc
2022-06-15 11:11:14 -04:00
Andrey Beletsky
e19facdded Add metadata to error returned by jsonrpc 2022-06-15 00:49:46 +07:00
Niko Storni
365d23f0e2 add support for deterministic pub keys
fix a couple of bugs
2022-06-10 18:18:26 +02:00
Niko Storni
e5ab0f883e update dependencies and go 2022-05-04 18:27:35 +02:00
Alex Grin
d0aeb0c22b
Merge pull request #91 from lbryio/bugfix/jeffreypicard/handle_colons_correctly 2022-03-21 14:25:39 -04:00
Jeffrey Picard
306db74279 We no longer use colons for sequence numbers in urls 2022-03-18 16:27:02 -04:00
Mark Beamer Jr
a0391bec79
Extend claim search for use in livestreaming 2022-02-08 16:00:38 -05:00
Alex Grin
5d62502bde
Update readme.md 2022-01-17 09:40:58 -05:00
Alex Grin
91ac7abf08
Merge pull request #90 from lbryio/fix_dht 2021-10-05 09:03:23 -04:00
Victor Shyba
8161f48c15 apply gofmt 2021-10-04 23:21:59 -03:00
Victor Shyba
d11230aaf8 show results over RPC 2021-10-03 04:53:41 -03:00
Victor Shyba
8fd87dfc31 parse page and always try to parse what is left as the result 2021-10-03 04:53:36 -03:00
Victor Shyba
4056c44c2e encode contacts as hex to be friendly on RPC return 2021-10-03 04:49:33 -03:00
Victor Shyba
dd451eb72b alpha was increased to 5 2021-10-03 04:49:04 -03:00
Alex Grin
a553e18d3b
Update readme.md 2021-09-28 10:15:05 -04:00
Niko Storni
3e18b74da0 fix stream by magic
upgrade to go 1.16
2021-08-24 11:46:06 -04:00
Mark Beamer Jr
55dceeaa4e
Add OAuth client for internal-apis to be used for odysee-apis 2021-08-08 14:09:19 -04:00
Mark Beamer Jr
a1177c17d3
Status error should default to internal server error. Otherwise it will trigger a nil pointer and subsequent panic. 2021-08-08 14:09:19 -04:00
Alex Grin
2b155597bf
Merge pull request #89 from jeffreypicard/add_utility_funcs 2021-06-25 10:50:58 -04:00
Jeffrey Picard
87bf89a109 Cleanup utility functions and add comments 2021-06-23 15:29:23 -04:00
Jeffrey Picard
931d786c52 Add utility functions from hub 2021-06-17 23:59:06 -04:00
Mark Beamer Jr
6516df1418
Add signing channel to transaction (transaction_show) 2021-04-16 15:53:22 -04:00
Mark Beamer Jr
3027fb9b98
Add transaction show API to library. 2021-04-15 16:03:45 -04:00
Niko Storni
ed51ece75c revert is_spent changes because booleans suddenly have a single state
present (doesn't matter if true or false) and missing.
TIL /s
2021-04-13 00:29:18 +02:00
Alex Grintsvayg
e00cdd0237
upgrade go-errors, which adds errors.Is compat 2021-04-02 14:24:09 -04:00
Alex Grintsvayg
6bc878d657
terminate stream after consuming all the data 2021-04-02 14:16:46 -04:00
Alex Grintsvayg
be64130ae1
json convenience method 2021-04-02 12:57:06 -04:00
Alex Grintsvayg
419e7c88a3
switch to io.Reader interface for stream creation 2021-04-01 17:01:49 -04:00
Mark Beamer Jr
988178df50
Move signature to keys package 2021-03-15 20:00:44 -04:00
Mark Beamer Jr
a365d63d16
Fix up PrivateKeyToDER function and add tests 2021-03-14 12:26:53 -04:00
Mark Beamer Jr
bd452c421f
Add PrivateKeyToDER function 2021-03-14 04:17:27 -04:00
Niko Storni
4c3372992c improve claim listing 2021-03-12 02:06:52 +01:00
Mark
3c99b84721
Merge pull request #86 from lbryio/cors
Cors
2021-03-10 21:04:16 -05:00
Mark Beamer Jr
d7e84c6b97
Add CORS to api server configuration 2021-03-10 20:55:59 -05:00
Mark Beamer Jr
4580a95b74
Add CORS to api server 2021-03-10 20:04:48 -05:00
Andrey Beletsky
29773829af
Merge pull request #85 from lbryio/lbryinc-errors
Lbryinc errors
2021-02-23 00:37:59 +07:00
Andrey Beletsky
ef1b43ac62 Do not treat server errors as API originated errors 2021-02-16 19:40:18 +07:00
Andrey Beletsky
39e5821760 Run gofmt on validate.go 2021-02-16 18:38:34 +07:00
Andrey Beletsky
cb68cb004e Fix travis go version 2021-02-16 18:34:53 +07:00
Andrey Beletsky
eb6bb93500 Discern API errors from transport level errors 2021-02-16 18:30:09 +07:00
Andrey Beletsky
d0df93ebac Update go to 1.15, update testify library 2021-02-16 18:29:41 +07:00
Alex Grintsvayg
8c41d8ccd9
add akins url parsing 2020-12-22 16:31:18 -05:00
Mark
e9753ffdc7
Merge pull request #84 from lbryio/stake_supports
Rename packages to represent stakes (claims || supports)
2020-11-18 15:36:19 -05:00
Mark Beamer Jr
69e03da94a
Add extraction of PrivateKey from pem 2020-11-18 01:30:50 -05:00
Mark Beamer Jr
b3f7657c1b
Rename packages to represent stakes (claims || supports) instead of just claim 2020-10-28 03:28:48 -04:00
Mark Beamer Jr
29574578c1
Add strings.go utility 2020-09-01 14:36:59 -04:00
Niko Storni
73382bb021 fix lbry.go dep ver 2020-09-01 19:58:08 +02:00
Niko Storni
69e2f6231c update modules 2020-09-01 19:55:06 +02:00
Alex Grintsvayg
fb88808c97
update lbryschema imports 2020-09-01 13:45:55 -04:00
Alex Grintsvayg
a16797cc53
merge in lbryschema.go 2020-09-01 13:31:15 -04:00
Alex Grintsvayg
b14fb6c18b
prep repo for merge into lbry.go 2020-09-01 13:30:36 -04:00
Niko Storni
8db975b532 fix go modules 2020-09-01 18:46:56 +02:00
Alex Grintsvayg
fcade74753
export query.Placeholders 2020-07-10 14:01:40 -04:00
Niko Storni
f1d8bc0ffc Merge branch 'improvement/get_response' 2020-06-03 22:12:05 +02:00
Andrey Beletsky
9d8b9330f9 Add PurchaseReceipt to Claim 2020-06-03 22:11:42 +02:00
Andrey Beletsky
3ae040d677 Remove a reference to points_paid field in get response 2020-06-03 22:11:42 +02:00
Alex Grintsvayg
07d2d00b0d
add wallet id to transaction_list call 2020-05-14 16:53:22 -04:00
Niko Storni
86d0678274 align SDK to v0.73.1 2020-05-14 05:39:51 +02:00
Niko Storni
bdf6240ed8 fix json value (woops) 2020-04-01 04:26:27 +02:00
Niko Storni
e8b93e3bb1 fix utxo list response 2020-04-01 04:23:59 +02:00
Niko Storni
9a9be92d27 include full transaction response 2020-03-31 04:11:44 +02:00
Niko Storni
414be62d61 add support for txo_spend 2020-03-31 04:05:43 +02:00
Alex Grintsvayg
21017a38a7
update dht seed nodes 2020-03-16 11:19:19 -04:00
Mark Beamer Jr
1a30fb743b
Update Simple Send to work with internal-apis rewards. 2020-02-04 00:32:43 -05:00
Thomas Zarebczan
db8aa21b35
Merge pull request #79 from ykris45/patch-1
Update LICENSE
2020-02-03 17:13:08 -05:00
Mark
c4772e61c5
Merge pull request #80 from lbryio/lbrycrd_go
Add chain params to lbrycrd client creation
2020-02-03 00:35:42 -05:00
Mark Beamer Jr
9541d765a9
Add chain params to lbrycrd client creation
Add Bech notation to alternative chain params
2020-02-03 00:12:45 -05:00
YULIUS KURNIAWAN KRISTIANTO
f9717328bc
Update LICENSE 2020-02-03 05:59:07 +07:00
Alex Grintsvayg
1835deb2c9
fix mainnet bech32 prefix 2020-01-08 20:45:10 -05:00
Mark Beamer Jr
dc6b15a372
add floats 2020-01-05 03:31:47 -05:00
Mark Beamer Jr
635bf931c8
Add PtrToNullTime 2019-12-23 01:51:02 -05:00
Andrey Beletsky
d12e431b40 Add has_verified_email method to lbryinc client 2019-12-17 21:06:52 +07:00
Mark Beamer Jr
8f8b2e605b
Add ability to specify the format of the query parameters via json tag. 2019-12-15 01:37:44 -05:00
Mark Beamer Jr
9b6fea461c
Add ability to control the format of the json response from api server. 2019-12-11 21:21:21 -05:00
Mark Beamer Jr
a391c83a2f
Add ordered_map to lbry.go for use in other repositories. 2019-12-11 20:39:38 -05:00
Niko Storni
06764c3d00
add support for SDK 0.48.1 2019-12-06 14:43:19 -05:00
Niko Storni
b68c49ac2f big brain fart, wrong repo 2019-11-20 11:16:13 -05:00
Niko Storni
96ace4b850 disable click tracking for verifications 2019-11-20 11:15:03 -05:00
Niko Storni
6782b19c3d Merge branch 'fund-ids' 2019-11-19 19:31:53 -05:00
Niko Storni
bcadcd1eca add funding ids for claim operations 2019-11-12 18:57:10 -05:00
Niko
ef1b10b601
Merge pull request #75 from lbryio/replace-v2
V2 update
2019-10-25 16:49:59 +02:00
Andrey Beletsky
aec378bb36 Fix failing wallet command test 2019-10-18 12:40:17 +07:00
Niko Storni
fa9bab156a revert btcutil update 2019-10-10 16:41:33 +02:00
Niko Storni
de5b32b1b5 update all modules 2019-10-10 05:16:23 +02:00
Niko Storni
dd2171172d update to v2 2019-10-10 05:07:33 +02:00
Niko Storni
1155ea6b9d add stream_list 2019-10-10 04:29:11 +02:00
Alex Grin
6918a2436a
Merge pull request #74 from StrikerRUS/patch-1
bump year in license
2019-10-09 10:37:13 -04:00
Nikita Titov
7e524c6b08
bump year in license 2019-10-09 00:06:05 +03:00
sayplastic
9c278d131d
Merge pull request #73 from lbryio/feature/wallet
Add wallet commands to JSON-RPC client
2019-10-08 13:08:57 +07:00
Niko Storni
b1c090e28d fix claim search not returning claim values 2019-10-03 22:50:07 +02:00
Andrey Beletsky
b03e0a7db8 Go mod tidy 2019-10-04 01:09:42 +07:00
Andrey Beletsky
33071ff6c1 Add test for wallet-less ChannelImport call 2019-10-04 00:51:24 +07:00
Andrey Beletsky
30a9a39248 Add ChannelImport method 2019-10-03 20:03:31 +07:00
Andrey Beletsky
49c9531404 Fix wallet opts passing 2019-10-02 14:56:37 +07:00
Andrey Beletsky
c68c7e05fe Bring back versioning to the module 2019-10-02 14:36:04 +07:00
Andrey Beletsky
534831e6e5 Improve args passing to SDK for WalletCreate command 2019-10-02 14:29:13 +07:00
Niko Storni
deab868c53 add bid to channel update params 2019-09-27 15:36:23 +02:00
Andrey Beletsky
d5102a9cf6 Add WalletList command 2019-09-25 18:00:37 +07:00
Andrey Beletsky
ac75979453 Add wallet commands to JSON-RPC client 2019-09-25 17:47:12 +07:00
Niko Storni
712e346bd2 woopsy 2019-09-24 20:37:03 +02:00
Niko Storni
85e34cb335 add accountAdd support
refactor account management
2019-09-24 20:35:04 +02:00
Niko Storni
c36c67961f Merge branch 'accountid-and-missing' 2019-09-24 18:31:17 +02:00
Niko Storni
025c715ab4 align to new SDK v0.42.0 2019-09-24 18:31:01 +02:00
Thomas Zarebczan
af728f12d9 add new types
Wasn't sure about type `UTXOReleaseResponse *string` 

This is the response: ```
{
  "jsonrpc": "2.0",
  "result": null
}
```
2019-09-24 16:04:13 +02:00
Thomas Zarebczan
1f1848a408 Adds account_id to channel create (and update), missing functions 2019-09-24 16:04:13 +02:00
Niko Storni
d032c842d5 remove problematic data 2019-09-24 16:03:49 +02:00
Niko Storni
d6f5199acd tidy up go mods 2019-09-24 16:02:26 +02:00
sayplastic
28aad86e4a
Merge pull request #72 from lbryio/feature/remote_ip
Forward real IP in internal-api client calls
2019-09-20 13:52:07 +07:00
Andrey Beletsky
c5c634e477 Pass user method url as a constant 2019-09-20 13:47:45 +07:00
Andrey Beletsky
a8c339e5b4 Forward real IP in internal-api client calls 2019-09-18 18:02:03 +07:00
Alex Grintsvayg
fd916d9eae
expose blob hash sizes as constants 2019-09-10 16:42:36 -04:00
Niko Storni
2d45f059ec improve error handling on json marshalling 2019-09-10 16:07:44 +02:00
sayplastic
e5850035dd
Merge pull request #70 from lbryio/feature/stream_create_account_id
Add account ID to StreamCreateOptions
2019-09-03 01:08:10 +07:00
Andrey Beletsky
0cfc8e230c Add account ID to StreamCreateOptions 2019-09-03 00:47:52 +07:00
Niko Storni
f3a1fbdd53 use the right library 2019-08-28 15:12:28 +02:00
Mark
ecbd404da0
Merge pull request #69 from lbryio/supports
Add ClaimSupport ( tippable too)
2019-08-27 23:12:44 -04:00
Mark Beamer Jr
2768cdd312
Add ClaimSupport ( tippable too) 2019-08-27 21:31:39 -04:00
Mark
564595cfc3
Merge pull request #66 from lbryio/claims
Add claim management for lbrycrd client
2019-08-27 21:05:07 -04:00
Niko Storni
3a3377d0e5 add support related commands
improve tests
2019-08-27 00:25:47 +02:00
Niko Storni
38861421f8 Merge branch '0.39.0_changes' 2019-08-26 23:15:58 +02:00
Mark Beamer Jr
a7bb3cf336 Update AccountBalanceResponse 2019-08-26 23:15:47 +02:00
Mark Beamer Jr
8fa28d3d65
Remove wait function for Group - recursive call. 2019-08-25 16:20:01 -04:00
Mark Beamer Jr
f8a231286a
Remove listwaitingon from Wait call. Too much logging. 2019-08-25 16:14:50 -04:00
Mark Beamer Jr
ea5b70e8fc
Remove unlocked since its there already. 2019-08-25 16:12:21 -04:00
Mark Beamer Jr
26b0c7356d
Add additional logging to wait call to know the list of routines active when wait is called. 2019-08-25 16:09:50 -04:00
Mark Beamer Jr
7558397877
Add logging to address decode error
Add test for real main net address
Add name to mainnet chain params
2019-08-24 21:04:24 -04:00
Niko Storni
fb7d045753 fix claim_search 2019-08-16 13:45:40 +02:00
Mark Beamer Jr
4d17553a23
Add claim management for lbrycrd client 2019-08-14 00:44:27 -04:00
Alex Grintsvayg
40633c949e
GetInfo() dropped in latest lbrycrdd 2019-08-07 08:40:26 -04:00
Mark
a17fa3ad5f
Merge pull request #64 from lbryio/channelexport
Add channel export call
2019-07-28 10:37:01 -04:00
Mark Beamer Jr
560858ba0a
Add channel export call 2019-07-28 10:05:18 -04:00
Niko Storni
ada0ce0484 don't panic if magic doesn't work 2019-07-10 17:57:04 +02:00
Mark
461ae6a16b
Merge pull request #61 from lbryio/go_sdk38_client_updates
Add updated structure for response api call based on SDK 38 release.
2019-07-09 21:47:30 -04:00
Niko Storni
31456e7bae remove timestamp for now 2019-06-25 21:20:29 -04:00
Niko Storni
060970b7c0 stupid IDE 2019-06-25 21:07:08 -04:00
Niko Storni
0f894aaecc fix timestamp in utxo list 2019-06-25 21:04:10 -04:00
Niko Storni
8367901104 fix utxo list response 2019-06-25 20:34:51 -04:00
Mark Beamer Jr
7e445e0cf8
Add updated structure for response api call based on SDK 38 release. 2019-06-24 00:29:15 -04:00
Niko Storni
f5de4e96c3 don't overwrite values when omitted 2019-06-12 22:18:03 +02:00
Niko Storni
6226c2690d really fix account_set 2019-06-12 05:23:48 +02:00
Niko Storni
3335233566 fix account_set 2019-06-12 04:43:27 +02:00
Niko Storni
faac895509 fix test
I hit push accidentally
2019-06-12 02:54:18 +02:00
Niko Storni
bc9886b3e8 add ability to use everything for account_fund 2019-06-12 02:53:42 +02:00
Niko Storni
963177cd4c add blocking feature to certain SDK calls 2019-06-11 18:40:48 +02:00
Mark Beamer Jr
6d2f69a36f
remove address validation 2019-06-02 13:32:30 -04:00
Niko Storni
b444d284c6 align params to sdk 2019-06-01 03:38:48 +02:00
Niko Storni
f853f2a00b prevent overwriting properties when values not passed 2019-06-01 03:17:10 +02:00
Niko Storni
a8afd1621e fix stream abandon 2019-05-30 00:18:23 +02:00
Niko Storni
88aad289cf extra fixes 2019-05-28 19:27:04 +02:00
Niko Storni
de1476becf align to SDK release
fix bugs
2019-05-28 18:29:14 +02:00
sayplastic
6ae869bdb3
Merge pull request #60 from lbryio/feature/account_remove
Add singular account_list method, account_remove
2019-05-24 22:06:04 +07:00
Andrey Beletsky
818b00190d Cleanup accounts jsonrpc code 2019-05-24 21:52:04 +07:00
Andrey Beletsky
630260d0b1 Fix AccountRemove test 2019-05-23 00:58:42 +07:00
Andrey Beletsky
15a137c502 Tidy up go.mod 2019-05-22 22:44:47 +07:00
Andrey Beletsky
438be5767a Remove Account from AccountCreateResponse 2019-05-22 18:32:20 +07:00
Andrey Beletsky
6c7f59bfb8 Fix AccountRemoveResponse declaration 2019-05-22 11:45:10 +07:00
Andrey Beletsky
7220ec8943 Use Account struct instead of derived SingleAccountListResponse 2019-05-22 11:40:18 +07:00
Andrey Beletsky
3615ab5e55 Attempt to fix account_create method 2019-05-22 01:17:22 +07:00
Andrey Beletsky
7ba00dffcc Add singular account_list method, account_remove 2019-05-22 00:12:00 +07:00
Niko Storni
76b73ef0c7 rollback btcd libraries 2019-05-16 22:46:27 +02:00
sayplastic
5424746066
Merge pull request #59 from lbryio/feature/lbryinc3
Add internal-apis client
2019-05-10 20:16:04 +07:00
Andrey Beletsky
b6b411847a Fix lbryinc client stylistics after code review 2019-05-10 20:10:56 +07:00
Andrey Beletsky
6bda111ffa Add internal-apis client 2019-05-10 19:47:49 +07:00
Niko Storni
77ea5aa522 update readme with versioning instructions
fix travis
2019-05-10 14:37:16 +02:00
Niko
abb34bf2dc
Merge pull request #50 from lbryio/lbrynet-updates
lbrynet SDK updates (support for lbrynet 0.37.*)
2019-05-10 14:16:28 +02:00
Niko Storni
c3aeba7e17 add deploy script for semver 2019-05-10 14:10:56 +02:00
Niko Storni
8e52397bb4
cleanup branch 2019-05-10 14:04:17 +02:00
Niko Storni
ebc3b6a55d
add channelUpdate
fix channel create params
2019-05-10 14:04:17 +02:00
Niko Storni
7b0a5ae028
update claim structure
fix bug in stream update
2019-05-10 14:04:17 +02:00
Niko Storni
13ef7571b2
parameterize blockchain name 2019-05-10 14:04:16 +02:00
Niko Storni
b63ee94973
add GetStreamSize
BY MAGIC!

add fileSize for stream update

fix mistake
2019-05-10 14:04:16 +02:00
Andrey Beletsky
2ae66ad3f7
Update lbryschema version
add file size param
2019-05-10 14:04:14 +02:00
Mark Beamer Jr
52702e2226
Added support for 0.36.0 of lbrynet.
upgraded to latest types/schema repo

fix several bugs

(this only works on mainnet)
2019-05-10 14:04:13 +02:00
Niko Storni
f827da4c61
reinstate claim type
fix bug

adjust types

add temporary video_duration until the SDK patches it
2019-05-10 14:04:12 +02:00
Niko Storni
3d2986a85c
add accountSet
fix bugs

fix rebase conflicts
2019-05-10 14:04:08 +02:00
Mark Beamer Jr
1d3e6c524b
removed Claim_Type_name as it is no longer used in latest types repo
more fixes
2019-05-10 14:04:04 +02:00
Andrey Beletsky
5f847fb035
Add AccountCreate method 2019-05-10 14:04:02 +02:00
Niko Storni
6f0d34f863
Update to ALMOST support 0.35 with new metadata 2019-05-10 14:04:02 +02:00
Niko Storni
41b4a3684a
update status response
wording

Make claim_sequence a signed int
2019-05-10 14:04:02 +02:00
Niko Storni
38d493fdd1
upgrade lbrynet to support 0.32.* 2019-05-10 14:03:58 +02:00
Niko Storni
d33ac919d1
fix resolution
add broadcast flag
2019-05-10 14:03:58 +02:00
Niko Storni
ec0eec8ac4
ultimate needed SDK calls update
removed useless pagination

add type to claim signature
2019-05-10 14:03:56 +02:00
Niko Storni
2437a06505
add channelNew and claimAbandon
simplified code
2019-05-10 14:03:46 +02:00
Niko Storni
e90b6dfcc7
add more SDK calls
export jsonrpc.Decode
add dep as dependencies manager
2019-05-10 14:03:46 +02:00
Niko Storni
c0a12af3ae
add 3 new SDK calls 2019-05-10 14:03:46 +02:00
Mark Beamer Jr
d701bab7f6
Fixed claim signing after changes in 90bef98bc3 2019-05-04 00:00:23 -04:00
Mark Beamer Jr
c54836bca0
Added based types to serialization. 2019-04-28 19:10:07 -04:00
Mark Beamer Jr
322c658307
Refactored to work with types repo @3af92598 2019-04-21 23:06:48 -04:00
Mark Beamer Jr
16f251b7f3
ok my mistake, back to 0.5.0 - will update chainquery. 2019-04-14 00:25:15 -04:00
Mark Beamer Jr
ce30f66823
lowered slack api version to 0.2.0 - upgrading is a breaking change. 2019-04-14 00:13:26 -04:00
Mark Beamer Jr
3d72de1cec
lowered slack api version 2019-04-14 00:08:39 -04:00
Mark Beamer Jr
bad2d869f5
Added claim_id for v1 claim migration 2019-04-13 23:23:41 -04:00
Mark Beamer Jr
846f1afaab
Updated lbry.go to the latest types version 2019-04-11 14:45:31 -04:00
Mark
414540ec6d
Merge pull request #7 from lbryio/proto3
added support for proto3 metadata definitions
2019-04-11 00:15:10 -04:00
Mark Beamer Jr
9159c7602d
added support for proto3 metadata definitions
added signing capabilities
all with unit tests
2019-04-07 00:29:12 -04:00
Alex Grintsvayg
211c2884c2
lbry.com switch 2019-03-18 12:26:43 -04:00
Niko Storni
74be347b9e
Merge branch 'interpolation' 2019-03-08 19:15:03 +01:00
Mark
50996a6b0d
Merge pull request #5 from lbryio/remove_migratedfrom
removed migratedFrom field on ClaimHelper, and added unit tests.
2018-11-09 21:39:47 -05:00
Mark Beamer Jr
e29b47a047
removed migratedFrom field on ClaimHelper, and added unit tests. 2018-11-09 21:37:59 -05:00
Mark
38325c6d44
Merge pull request #3 from lbryio/add_migration
Added migration method.
2018-11-09 17:15:53 -05:00
Mark Beamer Jr
c84b36d76d
added travis support. 2018-11-09 17:09:38 -05:00
Mark Beamer Jr
95c75ed957
Added negative test cases. 2018-11-09 17:09:34 -05:00
Mark Beamer Jr
ccd1b2b84d
Added migration method and cleaned things up. 2018-11-09 17:07:47 -05:00
Mark
dac2dbda61
Merge pull request #4 from lbryio/move_to_types
moved to lbryio/types/go
2018-11-09 17:05:07 -05:00
Mark Beamer Jr
2e58228e91
moved to lbryio/types/go 2018-11-09 17:04:05 -05:00
Jack Robison
185433f2fd
Merge pull request #2 from roylee17/fix-concurrent-write-to-addressPrefix
avoid concurrent write to addressPrefixes
2018-05-15 18:53:26 -04:00
Tzu-Jung Lee
40f9c3f961 avoid concurrent write to addressPrefixes 2018-05-15 10:13:05 -07:00
Jack Robison
dd814b834b
formatting and imports 2018-02-15 14:51:51 -05:00
Jack Robison
cdaf3ac682
add SerializeClaimFromJSON , DecodeAddress, and EncodeAddress to python binding 2017-11-27 10:25:04 -05:00
Jack Robison
6d169425d1
split b58 checksum validation from address validation 2017-11-27 10:24:10 -05:00
Jack Robison
f53da5c3e5
add DecodeClaimJSON 2017-11-27 10:23:24 -05:00
Jack Robison
d89334f9eb
verify certificate fields 2017-11-27 10:22:57 -05:00
Jack Robison
2fcc2f106e
cleanup 2017-11-27 10:22:04 -05:00
Jack Robison
4cff2dd233
add testnet and regtest address validation 2017-11-20 13:44:19 -05:00
Jack Robison
37a8c4cae1
add DecodeClaimHex to python binding 2017-11-13 10:11:19 -05:00
Jack Robison
8aaa786512
add python binding 2017-11-08 22:01:32 -05:00
Jack Robison
1f23a260ae
Merge branch 'validate-signatures' 2017-11-08 20:28:10 -05:00
Jack Robison
e02762ab6c
add decode tool 2017-11-07 21:41:07 -05:00
Jack Robison
9a629bb545
validate SECP256k1 signatures 2017-11-07 21:39:08 -05:00
Jack Robison
c62c175d8b
more 2017-09-12 12:02:30 -04:00
Jack Robison
eb8f72f0d8
initial commit 2017-09-06 10:31:28 -04:00
90 changed files with 7167 additions and 951 deletions

View file

@ -1,18 +1,29 @@
os: linux os: linux
dist: xenial dist: bionic
language: go language: go
go: go:
- 1.11.x - 1.17.x
env: env:
- GO111MODULE=on global:
- GO111MODULE=on
#GITHUB_TOKEN
- secure: "P4YMfllsq/guf5swmBl80IHGvNfUM+5X2b+0LvQQxGJpY9TD6d+PW6U1C90HIT0CDk6UZbtlHyGN+oo+shsWnwMIaM4qmbGdRgtG4TZolXY1QtYQZFW9fTWeHM0wnJeXLV8V4vpTRHnkLrvT8PctbGp5pVOEtcV4y3sEnMP1rkxnMPeHtrDbTlpBmBYEziByEpbg3pe3CE9xVaDD9DqgW7VOOZnQQl7exTtjsIeJYI7OYvdidf/04p632/8WZP2EJbmA2HunfTydWtcD51lCF8V3IvhKKqH4/7sNOKfmUwTDyhLltk9eDcFsYR/FYsy8njH4QDBBrKo/bPq3jzgKF9BY7g28/jMTDY4vDY0Na+4a3+7sDqwdsZ+eiZrgWYUbZ4MwYtpUtcvp8gUz7Avs3v7BBgYsYpgLEW47bT6uNLGO+SdfSwBCYuxT5P0IGCv6wkgYcYgfqVafk+9FiwJlayP2j3wPOdauiYh4WnYX1Rt0zNvaiP5iBhkXwIv3VvUcI1Yu9k0eLDibzZlpR8fkGw3USl+omdjCvXDfJQW1ghMONTm0d7VPG84P/MRhJ+thoh1UwMaT3S0sdPznRnNL6XDntCRT1CZW4l0jfyCPDBW2qw8dWunULYsQZTPWnv4s+BuRRDsfGjyamH5b8evh0TOF+XNOFewmNvflWBNG2y0="
install: true install: true
script: script:
# Fail if a .go file hasn't been formatted with gofmt # Fail if a .go file hasn't been formatted with gofmt
- test -z $(gofmt -s -l $(find . -iname '*.go' -type f | grep -v /vendor/ )) - test -z $(gofmt -s -l $(find . -iname '*.go' -type f))
- make - make
notifications: notifications:
email: false email: false
deploy:
provider: script
skip_cleanup: true
script: ./scripts/deploy.sh
file: bin/lbry
on:
repo: lbryio/lbry.go
tags: true

View file

@ -1,6 +1,6 @@
The MIT License (MIT) 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: 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:

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"

View file

@ -3,7 +3,7 @@ package claim
import ( import (
"bytes" "bytes"
types "github.com/lbryio/types/v1/go" types "github.com/lbryio/types/v2/go"
"github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"

View file

@ -7,7 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"
) )

View file

@ -3,7 +3,7 @@ package bits
import ( import (
"math/big" "math/big"
"github.com/lbryio/errors.go" "github.com/lbryio/lbry.go/v2/extras/errors"
) )
// Range has a start and end // Range has a start and end

View file

@ -6,7 +6,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
const ( const (

View file

@ -4,7 +4,7 @@ import (
"net" "net"
"testing" "testing"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
func TestBootstrapPing(t *testing.T) { func TestBootstrapPing(t *testing.T) {

View file

@ -4,7 +4,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
const ( 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: 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 // 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 bucketSize = 8 // this is the constant k in the spec
nodeIDLength = bits.NumBytes // bytes. this is the constant B in the spec nodeIDLength = bits.NumBytes // bytes. this is the constant B in the spec
messageIDLength = 20 // bytes. messageIDLength = 20 // bytes.
@ -65,9 +65,10 @@ func NewStandardConfig() *Config {
return &Config{ return &Config{
Address: "0.0.0.0:" + strconv.Itoa(DefaultPort), Address: "0.0.0.0:" + strconv.Itoa(DefaultPort),
SeedNodes: []string{ SeedNodes: []string{
"lbrynet1.lbry.io:4444", "lbrynet1.lbry.com:4444",
"lbrynet2.lbry.io:4444", "lbrynet2.lbry.com:4444",
"lbrynet3.lbry.io:4444", "lbrynet3.lbry.com:4444",
"lbrynet4.lbry.com:4444",
}, },
PeerProtocolPort: DefaultPeerPort, PeerProtocolPort: DefaultPeerPort,
ReannounceTime: DefaultReannounceTime, ReannounceTime: DefaultReannounceTime,

View file

@ -2,12 +2,13 @@ package dht
import ( import (
"bytes" "bytes"
"encoding/json"
"net" "net"
"sort" "sort"
"strconv" "strconv"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"
) )
@ -41,6 +42,20 @@ func (c Contact) String() string {
return str 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 // 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 // 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) { func (c Contact) MarshalCompact() ([]byte, error) {

View file

@ -5,7 +5,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
func TestCompactEncoding(t *testing.T) { func TestCompactEncoding(t *testing.T) {

View file

@ -6,9 +6,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/stop" "github.com/lbryio/lbry.go/v2/extras/stop"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cast" "github.com/spf13/cast"

View file

@ -7,8 +7,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )

View file

@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
func TestNodeFinder_FindNodes(t *testing.T) { func TestNodeFinder_FindNodes(t *testing.T) {

View file

@ -7,8 +7,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"
"github.com/spf13/cast" "github.com/spf13/cast"
@ -40,6 +40,7 @@ const (
headerPayloadField = "3" headerPayloadField = "3"
headerArgsField = "4" headerArgsField = "4"
contactsField = "contacts" contactsField = "contacts"
pageField = "p"
tokenField = "token" tokenField = "token"
protocolVersionField = "protocolVersion" protocolVersionField = "protocolVersion"
) )
@ -270,6 +271,7 @@ type Response struct {
FindValueKey string FindValueKey string
Token string Token string
ProtocolVersion int ProtocolVersion int
Page uint8
} }
func (r Response) argsDebug() string { func (r Response) argsDebug() string {
@ -390,27 +392,34 @@ func (r *Response) UnmarshalBencode(b []byte) error {
if contacts, ok := rawData[contactsField]; ok { if contacts, ok := rawData[contactsField]; ok {
err = bencode.DecodeBytes(contacts, &r.Contacts) err = bencode.DecodeBytes(contacts, &r.Contacts)
delete(rawData, contactsField) // so it doesnt mess up findValue key finding below
if err != nil { if err != nil {
return err return err
} }
} else { }
for k, v := range rawData { if page, ok := rawData[pageField]; ok {
r.FindValueKey = k err = bencode.DecodeBytes(page, &r.Page)
var compactContacts [][]byte delete(rawData, pageField) // so it doesnt mess up findValue key finding below
err = bencode.DecodeBytes(v, &compactContacts) if err != nil {
return err
}
}
for k, v := range rawData {
r.FindValueKey = k
var compactContacts [][]byte
err = bencode.DecodeBytes(v, &compactContacts)
if err != nil {
return err
}
for _, compact := range compactContacts {
var c Contact
err = c.UnmarshalCompact(compact)
if err != nil { if err != nil {
return err return err
} }
for _, compact := range compactContacts { r.Contacts = append(r.Contacts, c)
var c Contact
err = c.UnmarshalCompact(compact)
if err != nil {
return err
}
r.Contacts = append(r.Contacts, c)
}
break
} }
break
} }
return nil return nil

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"

View file

@ -7,10 +7,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/stop" "github.com/lbryio/lbry.go/v2/extras/stop"
"github.com/lbryio/lbry.go/extras/util" "github.com/lbryio/lbry.go/v2/extras/util"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"

View file

@ -4,13 +4,13 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/crypto" "github.com/lbryio/lbry.go/v2/extras/crypto"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/stop" "github.com/lbryio/lbry.go/v2/extras/stop"
"github.com/sirupsen/logrus" "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 // 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

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lyoshenka/bencode" "github.com/lyoshenka/bencode"
) )

View file

@ -9,9 +9,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/stop" "github.com/lbryio/lbry.go/v2/extras/stop"
) )
// TODO: if routing table is ever empty (aka the node is isolated), it should re-bootstrap // TODO: if routing table is ever empty (aka the node is isolated), it should re-bootstrap

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/sebdah/goldie" "github.com/sebdah/goldie"
) )

View file

@ -7,8 +7,8 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
rpc2 "github.com/gorilla/rpc/v2" rpc2 "github.com/gorilla/rpc/v2"
@ -102,6 +102,7 @@ type RpcIterativeFindValueArgs struct {
type RpcIterativeFindValueResult struct { type RpcIterativeFindValueResult struct {
Contacts []Contact Contacts []Contact
FoundValue bool FoundValue bool
Values []Contact
} }
func (rpc *rpcReceiver) IterativeFindValue(r *http.Request, args *RpcIterativeFindValueArgs, result *RpcIterativeFindValueResult) error { 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
result.Contacts = foundContacts result.Contacts = foundContacts
result.FoundValue = found result.FoundValue = found
if found {
for _, contact := range foundContacts {
if contact.PeerPort > 0 {
result.Values = append(result.Values, contact)
}
}
}
return nil return nil
} }

View file

@ -3,7 +3,7 @@ package dht
import ( import (
"sync" "sync"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
) )
// TODO: expire stored data after tExpire time // TODO: expire stored data after tExpire time

View file

@ -7,8 +7,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
) )
var testingDHTIP = "127.0.0.1" var testingDHTIP = "127.0.0.1"

View file

@ -4,8 +4,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/stop" "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 // 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

View file

@ -9,8 +9,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/lbryio/lbry.go/dht/bits" "github.com/lbryio/lbry.go/v2/dht/bits"
"github.com/lbryio/lbry.go/extras/stop" "github.com/lbryio/lbry.go/v2/extras/stop"
) )
type tokenManager struct { type tokenManager struct {

View file

@ -6,9 +6,9 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/util" "github.com/lbryio/lbry.go/v2/extras/util"
"github.com/lbryio/lbry.go/extras/validator" "github.com/lbryio/lbry.go/v2/extras/validator"
v "github.com/lbryio/ozzo-validation" v "github.com/lbryio/ozzo-validation"
"github.com/spf13/cast" "github.com/spf13/cast"
@ -17,9 +17,28 @@ import (
// ResponseHeaders are returned with each response // ResponseHeaders are returned with each response
var ResponseHeaders map[string]string 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 // Log allows logging of events and errors
var Log = func(*http.Request, *Response, error) {} 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. // TraceEnabled Attaches a trace field to the JSON response when enabled.
var TraceEnabled = false var TraceEnabled = false
@ -64,6 +83,32 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(key, value) 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 // Stop here if its a preflighted OPTIONS request
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
@ -76,6 +121,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rsp.Error != nil { if rsp.Error != nil {
ogErr := errors.Unwrap(rsp.Error) ogErr := errors.Unwrap(rsp.Error)
if statusError, ok := ogErr.(StatusError); ok { if statusError, ok := ogErr.(StatusError); ok {
if statusError.Status == 0 {
statusError.Status = http.StatusInternalServerError
}
rsp.Status = statusError.Status rsp.Status = statusError.Status
} else { } else {
rsp.Status = http.StatusInternalServerError rsp.Status = http.StatusInternalServerError
@ -113,32 +161,40 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var trace []string var trace []string
if TraceEnabled && errors.HasTrace(rsp.Error) { if TraceEnabled && errors.HasTrace(rsp.Error) {
trace = strings.Split(errors.Trace(rsp.Error), "\n") trace = getTraceFromError(rsp.Error)
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 := BuildJSONResponse(ResponseInfo{
jsonResponse, err := json.MarshalIndent(&struct {
Success bool `json:"success"`
Error *string `json:"error"`
Data interface{} `json:"data"`
Trace []string `json:"_trace,omitempty"`
}{
Success: success, Success: success,
Error: errorString, Error: errorString,
Data: rsp.Data, Data: rsp.Data,
Trace: trace, Trace: trace,
}, "", " ") })
if err != nil { if err != nil {
Log(r, &rsp, errors.Prefix("Error encoding JSON response: ", err)) 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.WriteHeader(rsp.Status)
w.Write(jsonResponse) _, _ = 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)
}
}
return trace
} }
// IgnoredFormFields are ignored by FormValues() when checking for extraneous fields // 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() structValue := ref.Elem()
fields := map[string]bool{} fields := map[string]bool{}
for i := 0; i < structType.NumField(); i++ { for i := 0; i < structType.NumField(); i++ {
name := structType.Field(i).Name fieldName := structType.Field(i).Name
underscoredName := util.Underscore(name) formattedName := util.Underscore(fieldName)
value := strings.TrimSpace(r.FormValue(underscoredName)) 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 // if param is not set at all, continue
// comes after call to r.FormValue so form values get parsed internally (if they arent already) // 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 continue
} }
fields[underscoredName] = true fields[formattedName] = true
isPtr := false isPtr := false
var finalValue reflect.Value var finalValue reflect.Value
structField := structValue.FieldByName(name) structField := structValue.FieldByName(fieldName)
structFieldKind := structField.Kind() structFieldKind := structField.Kind()
if structFieldKind == reflect.Ptr { if structFieldKind == reflect.Ptr {
isPtr = true isPtr = true
@ -184,7 +244,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
} }
castVal, err := cast.ToInt64E(value) castVal, err := cast.ToInt64E(value)
if err != nil { if err != nil {
return errors.Err("%s: must be an integer", underscoredName) return errors.Err("%s: must be an integer", formattedName)
} }
switch structFieldKind { switch structFieldKind {
case reflect.Int: case reflect.Int:
@ -204,7 +264,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
} }
castVal, err := cast.ToUint64E(value) castVal, err := cast.ToUint64E(value)
if err != nil { 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 { switch structFieldKind {
case reflect.Uint: case reflect.Uint:
@ -224,7 +284,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
} }
if !validator.IsBoolString(value) { if !validator.IsBoolString(value) {
return errors.Err("%s: must be one of the following values: %s", 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)) 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) castVal, err := cast.ToFloat64E(value)
if err != nil { 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 { switch structFieldKind {
case reflect.Float32: case reflect.Float32:
@ -243,7 +303,7 @@ func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldR
finalValue = reflect.ValueOf(float64(castVal)) finalValue = reflect.ValueOf(float64(castVal))
} }
default: default:
return errors.Err("field %s is an unsupported type", name) return errors.Err("field %s is an unsupported type", fieldName)
} }
if isPtr { if isPtr {

View file

@ -7,7 +7,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/btcsuite/btcutil/base58" "github.com/btcsuite/btcutil/base58"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,845 @@
package jsonrpc package jsonrpc
import ( import (
"encoding/json"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"testing" "testing"
"time"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/v2/extras/util"
) )
func TestStatus(t *testing.T) { func prettyPrint(i interface{}) {
s, _ := json.MarshalIndent(i, "", "\t")
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(1, 20)
if err != nil {
t.Error(err)
return
}
account := (accounts.Items)[0].ID
balanceString, err := d.AccountBalance(&account)
if err != nil {
t.Error(err)
return
}
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, 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(1, 20)
if err != nil {
t.Error(err)
return
}
prettyPrint(*got)
}
func TestClient_SingleAccountList(t *testing.T) {
d := NewClient("")
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)
}
if account.Items[0].Name != name {
t.Fatalf("account name mismatch: %v != %v", account.Items[0].Name, name)
}
}
func TestClient_AccountBalance(t *testing.T) {
d := NewClient("")
got, err := d.AccountBalance(nil)
if err != nil {
t.Error(err)
return
}
prettyPrint(*got)
}
func TestClient_AddressUnused(t *testing.T) {
d := NewClient("")
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, 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)
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: 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{{
Country: util.PtrToString("CH"),
State: util.PtrToString("Ticino"),
City: util.PtrToString("Lugano"),
PostalCode: util.PtrToString("6900"),
Latitude: nil,
Longitude: nil,
}},
ThumbnailURL: util.PtrToString("https://scrn.storni.info/2019-01-18_16-37-39-098537783.png"),
AccountID: nil,
ClaimAddress: &address,
Preview: nil,
},
Fee: &Fee{
FeeCurrency: "LBC",
FeeAmount: decimal.NewFromFloat(1.0),
FeeAddress: &address,
},
Author: util.PtrToString("Niko"),
License: util.PtrToString("FREE"),
LicenseURL: nil,
ReleaseTime: nil,
Duration: nil,
Preview: nil,
AllowDuplicateName: nil,
ChannelName: nil,
ChannelID: util.PtrToString(channelID),
ChannelAccountID: nil,
})
if err != nil {
t.Error(err)
return
}
prettyPrint(*got)
}
func TestClient_ChannelUpdate(t *testing.T) {
d := NewClient("")
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: 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{{
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_ChannelAbandon(t *testing.T) {
d := NewClient("")
channelName := "@TestToDelete" + fmt.Sprintf("%d", time.Now().Unix())
channelResponse, err := d.ChannelCreate(channelName, 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
}
txID := channelResponse.Outputs[0].Txid
nout := channelResponse.Outputs[0].Nout
time.Sleep(10 * time.Second)
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, 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_StreamList(t *testing.T) {
_ = os.Setenv("BLOCKCHAIN_NAME", "lbrycrd_regtest")
d := NewClient("")
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)
}
func TestClient_Status(t *testing.T) {
d := NewClient("")
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, 1, 20)
if err != nil {
t.Error(err)
return
}
prettyPrint(*got)
}
func TestClient_Version(t *testing.T) {
d := NewClient("")
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://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(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("test1559058649")
if err != nil {
t.Error(err)
return
}
prettyPrint(*got)
}
func TestClient_AccountSet(t *testing.T) {
d := NewClient("")
accounts, err := d.AccountList(1, 20)
if err != nil {
t.Error(err)
return
}
account := (accounts.Items)[0].ID
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 := "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("")
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 err != nil {
if strings.Contains(err.Error(), "Couldn't find account:") {
prettyPrint(*removedAccount)
return
}
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.")
}
} }

View file

@ -1,11 +1,19 @@
package jsonrpc package jsonrpc
import ( import (
"encoding/hex"
"encoding/json" "encoding/json"
"io/ioutil"
"net/http"
"os"
"reflect" "reflect"
"strings"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
lbryschema "github.com/lbryio/types/v1/go" "github.com/lbryio/lbry.go/v2/stream"
schema "github.com/lbryio/lbry.go/v2/schema/stake"
lbryschema "github.com/lbryio/types/v2/go"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
@ -19,69 +27,48 @@ const (
) )
type Fee struct { type Fee struct {
Currency Currency `json:"currency"` FeeCurrency Currency `json:"fee_currency"`
Amount decimal.Decimal `json:"amount"` FeeAmount decimal.Decimal `json:"fee_amount"`
Address *string `json:"address"` FeeAddress *string `json:"fee_address"`
}
type Support struct {
Amount decimal.Decimal `json:"amount"`
Nout int `json:"nout"`
Txid string `json:"txid"`
}
type Claim struct {
Address string `json:"address"`
Amount decimal.Decimal `json:"amount"`
BlocksToExpiration int `json:"blocks_to_expiration"`
Category string `json:"category"`
ClaimID string `json:"claim_id"`
ClaimSequence int `json:"claim_sequence"`
Confirmations int `json:"confirmations"`
DecodedClaim bool `json:"decoded_claim"`
Depth int `json:"depth"`
EffectiveAmount decimal.Decimal `json:"effective_amount"`
ExpirationHeight int `json:"expiration_height"`
Expired bool `json:"expired"`
Height int `json:"height"`
Hex string `json:"hex"`
IsSpent bool `json:"is_spent"`
Name string `json:"name"`
Nout int `json:"nout"`
PermanentUrl string `json:"permanent_url"`
Supports []Support `json:"supports"`
Txid string `json:"txid"`
ValidAtHeight int `json:"valid_at_height"`
Value lbryschema.Claim `json:"value"`
Error *string `json:"error,omitempty"`
ChannelName *string `json:"channel_name,omitempty"`
HasSignature *bool `json:"has_signature,omitempty"`
SignatureIsValid *bool `json:"signature_is_valid,omitempty"`
} }
type File struct { type File struct {
ClaimID string `json:"claim_id"` AddedOn int64 `json:"added_on"`
Completed bool `json:"completed"` BlobsCompleted uint64 `json:"blobs_completed"`
DownloadDirectory string `json:"download_directory"` BlobsInStream uint64 `json:"blobs_in_stream"`
DownloadPath string `json:"download_path"` BlobsRemaining uint64 `json:"blobs_remaining"`
FileName string `json:"file_name"` ChannelClaimID string `json:"channel_claim_id"`
Key string `json:"key"` ChannelName string `json:"channel_name"`
Message string `json:"message"` ClaimID string `json:"claim_id"`
Metadata *lbryschema.Claim `json:"metadata"` ClaimName string `json:"claim_name"`
MimeType string `json:"mime_type"` Completed bool `json:"completed"`
Name string `json:"name"` Confirmations int64 `json:"confirmations"`
Outpoint string `json:"outpoint"` ContentFee *Fee `json:"content_fee"`
PointsPaid decimal.Decimal `json:"points_paid"` DownloadDirectory string `json:"download_directory"`
SdHash string `json:"sd_hash"` DownloadPath string `json:"download_path"`
Stopped bool `json:"stopped"` FileName string `json:"file_name"`
StreamHash string `json:"stream_hash"` Height int `json:"height"`
StreamName string `json:"stream_name"` IsFullyReflected bool `json:"is_fully_reflected"`
SuggestedFileName string `json:"suggested_file_name"` Key string `json:"key"`
TotalBytes uint64 `json:"total_bytes"` Value *lbryschema.Claim `json:"protobuf"`
WrittenBytes uint64 `json:"written_bytes"` MimeType string `json:"mime_type"`
ChannelName *string `json:"channel_name,omitempty"` Nout int `json:"nout"`
HasSignature *bool `json:"has_signature,omitempty"` Outpoint string `json:"outpoint"`
SignatureIsValid *bool `json:"signature_is_valid,omitempty"` 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"`
} }
func getEnumVal(enum map[string]int32, data interface{}) (int32, error) { func getEnumVal(enum map[string]int32, data interface{}) (int32, error) {
@ -128,126 +115,27 @@ func fixDecodeProto(src, dest reflect.Type, data interface{}) (interface{}, erro
return d, nil return d, nil
} }
case reflect.TypeOf(lbryschema.Metadata_Version(0)):
val, err := getEnumVal(lbryschema.Metadata_Version_value, data)
return lbryschema.Metadata_Version(val), err
case reflect.TypeOf(lbryschema.Metadata_Language(0)):
val, err := getEnumVal(lbryschema.Metadata_Language_value, data)
return lbryschema.Metadata_Language(val), err
case reflect.TypeOf(lbryschema.Stream_Version(0)):
val, err := getEnumVal(lbryschema.Stream_Version_value, data)
return lbryschema.Stream_Version(val), err
case reflect.TypeOf(lbryschema.Claim_Version(0)):
val, err := getEnumVal(lbryschema.Claim_Version_value, data)
return lbryschema.Claim_Version(val), err
case reflect.TypeOf(lbryschema.Claim_ClaimType(0)):
val, err := getEnumVal(lbryschema.Claim_ClaimType_value, data)
return lbryschema.Claim_ClaimType(val), err
case reflect.TypeOf(lbryschema.Fee_Version(0)):
val, err := getEnumVal(lbryschema.Fee_Version_value, data)
return lbryschema.Fee_Version(val), err
case reflect.TypeOf(lbryschema.Fee_Currency(0)): case reflect.TypeOf(lbryschema.Fee_Currency(0)):
val, err := getEnumVal(lbryschema.Fee_Currency_value, data) val, err := getEnumVal(lbryschema.Fee_Currency_value, data)
return lbryschema.Fee_Currency(val), err return lbryschema.Fee_Currency(val), err
case reflect.TypeOf(lbryschema.Claim{}):
blockChainName := os.Getenv("BLOCKCHAIN_NAME")
if blockChainName == "" {
blockChainName = "lbrycrd_main"
}
case reflect.TypeOf(lbryschema.Source_Version(0)): claim, err := schema.DecodeClaimHex(data.(string), blockChainName)
val, err := getEnumVal(lbryschema.Source_Version_value, data) if err != nil {
return lbryschema.Source_Version(val), err return nil, err
case reflect.TypeOf(lbryschema.Source_SourceTypes(0)): }
val, err := getEnumVal(lbryschema.Source_SourceTypes_value, data) return claim.Claim, nil
return lbryschema.Source_SourceTypes(val), err
case reflect.TypeOf(lbryschema.KeyType(0)):
val, err := getEnumVal(lbryschema.KeyType_value, data)
return lbryschema.KeyType(val), err
case reflect.TypeOf(lbryschema.Signature_Version(0)):
val, err := getEnumVal(lbryschema.Signature_Version_value, data)
return lbryschema.Signature_Version(val), err
case reflect.TypeOf(lbryschema.Certificate_Version(0)):
val, err := getEnumVal(lbryschema.Certificate_Version_value, data)
return lbryschema.Certificate_Version(val), err
} }
return data, nil return data, nil
} }
type CommandsResponse []string
type WalletBalanceResponse decimal.Decimal type WalletBalanceResponse decimal.Decimal
type VersionResponse struct {
Build string `json:"build"`
LbrynetVersion string `json:"lbrynet_version"`
LbryschemaVersion string `json:"lbryschema_version"`
LbryumVersion string `json:"lbryum_version"`
OsRelease string `json:"os_release"`
OsSystem string `json:"os_system"`
Platform string `json:"platform"`
Processor string `json:"processor"`
PythonVersion string `json:"python_version"`
}
type StatusResponse struct {
BlobManager struct {
FinishedBlobs int `json:"finished_blobs"`
} `json:"blob_manager"`
ConnectionStatus struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"connection_status"`
Dht struct {
NodeID string `json:"node_id"`
PeersInRoutingTable int `json:"peers_in_routing_table"`
} `json:"dht"`
FileManager struct {
ManagedFiles int `json:"managed_files"`
} `json:"file_manager"`
HashAnnouncer struct {
AnnounceQueueSize int `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"`
PaymentRateManager bool `json:"payment_rate_manager"`
PeerProtocolServer bool `json:"peer_protocol_server"`
RateLimiter bool `json:"rate_limiter"`
StreamIdentifier bool `json:"stream_identifier"`
Upnp bool `json:"upnp"`
Wallet bool `json:"wallet"`
} `json:"startup_status"`
Wallet struct {
BestBlockchain string `json:"best_blockchain"`
Blocks int `json:"blocks"`
BlocksBehind int `json:"blocks_behind"`
IsEncrypted bool `json:"is_encrypted"`
} `json:"wallet"`
}
type ClaimListResponse struct {
Claims []Claim `json:"claims"`
LastTakeoverHeight int `json:"last_takeover_height"`
SupportsWithoutClaims []Support `json:"supports_without_claims"`
}
type ClaimListMineResponse []Claim
type ClaimShowResponse Claim
type ClaimAbandonResponse struct {
Txid string `json:"txid"`
Fee float64 `json:"fee"`
}
type PeerListResponsePeer struct { type PeerListResponsePeer struct {
IP string `json:"host"` IP string `json:"host"`
Port uint `json:"port"` Port uint `json:"port"`
@ -293,84 +181,490 @@ type StreamAvailabilityResponse struct {
} }
type GetResponse File type GetResponse File
type FileListResponse []File type FileListResponse struct {
Items []File `json:"items"`
type ResolveResponse map[string]ResolveResponseItem Page uint64 `json:"page"`
type ResolveResponseItem struct { PageSize uint64 `json:"page_size"`
Certificate *Claim `json:"certificate,omitempty"` TotalPages uint64 `json:"total_pages"`
Claim *Claim `json:"claim,omitempty"`
ClaimsInChannel *uint64 `json:"claims_in_channel,omitempty"`
Error *string `json:"error,omitempty"`
} }
type ChannelNewResponse struct { type Account struct {
ClaimID string `json:"claim_id"` AddressGenerator struct {
Fee decimal.Decimal `json:"fee"` Change struct {
Nout int `json:"nout"` Gap uint64 `json:"gap"`
Success bool `json:"success"` MaximumUsesPerAddress uint64 `json:"maximum_uses_per_address"`
Tx string `json:"tx"` } `json:"change"`
Txid string `json:"txid"` Name string `json:"name"`
Receiving struct {
Gap uint64 `json:"gap"`
MaximumUsesPerAddress uint64 `json:"maximum_uses_per_address"`
} `json:"receiving"`
} `json:"address_generator"`
Certificates uint64 `json:"certificates"`
Coins float64 `json:"coins"`
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 ChannelListSingleResponse struct { type AccountListResponse struct {
Items []Account `json:"items"`
Page uint64 `json:"page"`
PageSize uint64 `json:"page_size"`
TotalPages uint64 `json:"total_pages"`
}
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 Transaction struct {
Address string `json:"address"` Address string `json:"address"`
Amount decimal.Decimal `json:"amount"` Amount string `json:"amount"`
BlocksToExpiration int `json:"blocks_to_expiration"`
CanSign bool `json:"can_sign"`
Category string `json:"category"`
ClaimID string `json:"claim_id"` ClaimID string `json:"claim_id"`
ClaimOp string `json:"claim_op"`
Confirmations int `json:"confirmations"` Confirmations int `json:"confirmations"`
DecodedClaim bool `json:"decoded_claim"` HasSigningKey bool `json:"has_signing_key"`
ExpirationHeight int `json:"expiration_height"`
Expired bool `json:"expired"`
HasSignature bool `json:"has_signature"`
Height int `json:"height"` Height int `json:"height"`
Hex string `json:"hex"` IsInternalTransfer bool `json:"is_internal_transfer"`
IsPending bool `json:"is_pending"` IsMyInput bool `json:"is_my_input"`
IsMyOutput bool `json:"is_my_output"`
IsSpent bool `json:"is_spent"` IsSpent bool `json:"is_spent"`
Name string `json:"name"` Name string `json:"name"`
Nout int `json:"nout"` NormalizedName string `json:"normalized_name"`
Nout uint64 `json:"nout"`
PermanentUrl string `json:"permanent_url"`
SigningChannel *Claim `json:"signing_channel,omitempty"`
TimeStamp uint64 `json:"time_stamp"`
Txid string `json:"txid"` Txid string `json:"txid"`
Value *lbryschema.Claim `json:"value"` Type string `json:"type"`
Value *lbryschema.Claim `json:"protobuf,omitempty"`
} }
type ChannelListResponse []ChannelListSingleResponse type TransactionSummary struct {
Height int `json:"height"`
type WalletListResponse []string Hex string `json:"hex"`
Inputs []Transaction `json:"inputs"`
type PublishResponse struct { Outputs []Transaction `json:"outputs"`
ClaimID string `json:"claim_id"` TotalFee string `json:"total_fee"`
Fee decimal.Decimal `json:"fee"` TotalInput string `json:"total_input"`
Nout int `json:"nout"` TotalOutput string `json:"total_output"`
Tx string `json:"tx"` Txid string `json:"txid"`
Txid string `json:"txid"`
} }
type BlobAnnounceResponse bool type AccountFundResponse TransactionSummary
type WalletPrefillAddressesResponse struct { type Address string
Broadcast bool `json:"broadcast"` type AddressUnusedResponse Address
Complete bool `json:"complete"` type AddressListResponse struct {
Hex string `json:"hex"` 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 UTXOListResponse []struct { type ChannelExportResponse string
Address string `json:"address"` type ChannelImportResponse string
Amount decimal.Decimal `json:"amount"`
Height int `json:"height"` type ChannelListResponse struct {
IsClaim bool `json:"is_claim"` Items []Transaction `json:"items"`
IsCoinbase bool `json:"is_coinbase"` Page uint64 `json:"page"`
IsSupport bool `json:"is_support"` PageSize uint64 `json:"page_size"`
IsUpdate bool `json:"is_update"` TotalPages uint64 `json:"total_pages"`
Nout int `json:"nout"`
Txid string `json:"txid"`
} }
type WalletNewAddressResponse string type ClaimAbandonResponse struct {
Success bool `json:"success"`
type WalletUnusedAddressResponse string Tx TransactionSummary `json:"tx"`
}
type NumClaimsInChannelResponse map[string]struct { type Support struct {
ClaimsInChannel uint64 `json:"claims_in_channel,omitempty"` Amount string `json:"amount"`
Error string `json:"error,omitempty"` Nout uint64 `json:"nout"`
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"`
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"`
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"`
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"`
DecodedClaim bool `json:"decoded_claim,omitempty"`
EffectiveAmount string `json:"effective_amount,omitempty"`
HasSignature *bool `json:"has_signature,omitempty"`
SignatureIsValid *bool `json:"signature_is_valid,omitempty"`
Supports []Support `json:"supports,omitempty"`
ValidAtHeight int `json:"valid_at_height,omitempty"`
}
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() (streamSize uint64, e error) {
if c.Value.GetStream() == nil {
return 0, errors.Err("this claim is not a stream")
}
resp, err := http.Get(coldStorageURL + hex.EncodeToString(c.Value.GetStream().Source.SdHash))
if err != nil {
return 0, errors.Err(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, errors.Err(err)
}
sdb := &stream.SDBlob{}
err = sdb.UnmarshalJSON(body)
if err != nil {
return 0, err
}
lastBlobIndex := len(sdb.BlobInfos) - 2
lastBlobHash := sdb.BlobInfos[lastBlobIndex].BlobHash
if len(sdb.BlobInfos) > 2 {
streamSize = uint64(stream.MaxBlobSize-1) * uint64(len(sdb.BlobInfos)-2)
}
resp2, err := http.Get(coldStorageURL + hex.EncodeToString(lastBlobHash))
if err != nil {
return 0, errors.Err(err)
}
defer resp2.Body.Close()
body2, err := ioutil.ReadAll(resp2.Body)
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)
}
streamSize += uint64(len(lastBlob))
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"`
PageSize uint64 `json:"page_size"`
TotalPages uint64 `json:"total_pages"`
}
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 {
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"`
Message string `json:"message"`
} `json:"connection_status"`
Dht 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"`
IsRunning bool `json:"is_running"`
SkippedComponents []string `json:"skipped_components"`
StartupStatus struct {
BlobManager bool `json:"blob_manager"`
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"`
Upnp bool `json:"upnp"`
Wallet bool `json:"wallet"`
WalletServerPayments bool `json:"wallet_server_payments"`
} `json:"startup_status"`
Upnp struct {
AioupnpVersion string `json:"aioupnp_version"`
DhtRedirectSet bool `json:"dht_redirect_set"`
ExternalIp string `json:"external_ip"`
Gateway string `json:"gateway"`
PeerRedirectSet bool `json:"peer_redirect_set"`
Redirects struct{} `json:"redirects"`
} `json:"upnp"`
Wallet struct {
AvailableServers int `json:"available_servers"`
BestBlockhash string `json:"best_blockhash"`
Blocks int `json:"blocks"`
BlocksBehind int `json:"blocks_behind"`
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 {
Items []struct {
Address string `json:"address"`
Amount string `json:"amount"`
Confirmations int `json:"confirmations"`
Height int `json:"height"`
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 {
Build string `json:"build"`
Desktop string `json:"desktop"`
Distro struct {
Codename string `json:"codename"`
ID string `json:"id"`
Like string `json:"like"`
Version string `json:"version"`
VersionParts struct {
BuildNumber string `json:"build_number"`
Major string `json:"major"`
Minor string `json:"minor"`
} `json:"version_parts"`
} `json:"distro"`
LbrynetVersion string `json:"lbrynet_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]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"`
} }

269
extras/lbryinc/client.go Normal file
View file

@ -0,0 +1,269 @@
package lbryinc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"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 {
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 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 interface {
IsObject() bool
IsArray() bool
Object() (map[string]interface{}, error)
Array() ([]interface{}, error)
}
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, 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", 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")
}
form.Add(k, fmt.Sprintf("%v", v))
}
return form.Encode(), nil
}
func (c Client) doCall(url string, payload string) ([]byte, error) {
var body []byte
c.Logger.Debugf("sending payload: %s", payload)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer([]byte(payload)))
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)
}
// CallResource calls a remote internal-apis server resource, returning a response,
// wrapped into standardized API Response struct.
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 d, err
}
body, err := c.doCall(c.getEndpointURL(object, method), 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
}
// 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{}{})
}

View file

@ -0,0 +1,182 @@
package lbryinc
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"golang.org/x/oauth2"
"github.com/stretchr/testify/assert"
)
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))
}
}))
}
func TestUserMe(t *testing.T) {
ts := launchDummyServer(nil, makeMethodPath(userObjectPath, userMeMethod), userMeResponse, http.StatusOK)
defer ts.Close()
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": 12345,
"language": "en",
"given_name": null,
"family_name": null,
"created_at": "2019-01-17T12:13:06Z",
"updated_at": "2019-05-02T13:57:59Z",
"invited_by_id": null,
"invited_at": null,
"invites_remaining": 0,
"invite_reward_claimed": false,
"is_email_enabled": true,
"manual_approval_user_id": 654,
"reward_status_change_trigger": "manual",
"primary_email": "user@lbry.tv",
"has_verified_email": true,
"is_identity_verified": false,
"is_reward_approved": true,
"groups": []
}
}`
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"
}
]
}`

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

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

View file

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/extras/null" "github.com/lbryio/lbry.go/v2/extras/null"
) )
func InterpolateParams(query string, args ...interface{}) (string, error) { 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 // Qs is a shortcut for one group of positional placeholders
func Qs(count int) string { 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 // 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{} buf := bytes.Buffer{}
if start == 0 || group == 0 { if start == 0 || group == 0 {

View file

@ -101,8 +101,6 @@ func (s *Group) DoneNamed(name string) {
if s.waitingOn != nil { if s.waitingOn != nil {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock()
if current, ok := s.waitingOn[name]; ok { if current, ok := s.waitingOn[name]; ok {
if current <= 1 { if current <= 1 {
delete(s.waitingOn, name) delete(s.waitingOn, name)
@ -112,9 +110,16 @@ func (s *Group) DoneNamed(name string) {
} else { } else {
log.Printf("%s is not recorded in stop group map", name) 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") log.Printf("-->> LIST ROUTINES WAITING ON")
for k, v := range s.waitingOn { for k, v := range s.waitingOn {
if v > 0 { if v > 0 {
log.Printf("waiting on %d %s routines...", v, k) log.Printf("waiting on %d %s routines...", v, k)

View file

@ -23,7 +23,7 @@ import (
"encoding/pem" "encoding/pem"
"net/http" "net/http"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
) )
func publicKey(isPrivateRepo bool) (*rsa.PublicKey, error) { func publicKey(isPrivateRepo bool) (*rsa.PublicKey, error) {

59
extras/util/pointer.go Normal file
View file

@ -0,0 +1,59 @@
// mostly copied from github.com/AlekSi/pointer
// Provides helpers to get pointers to values of build-in types.
package util
import (
"time"
"github.com/lbryio/lbry.go/v2/extras/null"
)
func PtrToBool(b bool) *bool { return &b }
func PtrToByte(b byte) *byte { return &b }
func PtrToComplex128(c complex128) *complex128 { return &c }
func PtrToComplex64(c complex64) *complex64 { return &c }
func PtrToError(e error) *error { return &e }
func PtrToFloat32(f float32) *float32 { return &f }
func PtrToFloat64(f float64) *float64 { return &f }
func PtrToInt(i int) *int { return &i }
func PtrToInt16(i int16) *int16 { return &i }
func PtrToInt32(i int32) *int32 { return &i }
func PtrToInt64(i int64) *int64 { return &i }
func PtrToInt8(i int8) *int8 { return &i }
func PtrToRune(r rune) *rune { return &r }
func PtrToString(s string) *string { return &s }
func PtrToTime(t time.Time) *time.Time { return &t }
func PtrToUint(u uint) *uint { return &u }
func PtrToUint16(u uint16) *uint16 { return &u }
func PtrToUint32(u uint32) *uint32 { return &u }
func PtrToUint64(u uint64) *uint64 { return &u }
func PtrToUint8(u uint8) *uint8 { return &u }
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 {
return ""
}
return *ptr
}
func StrFromNull(str null.String) string {
if !str.Valid {
return ""
}
return str.String
}
func NullStringFrom(s string) null.String {
if s == "" {
return null.String{}
}
return null.StringFrom(s)
}

View file

@ -2,12 +2,15 @@ package util
import ( import (
"fmt" "fmt"
"net"
"net/http"
"strings" "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" log "github.com/sirupsen/logrus"
"github.com/slack-go/slack"
) )
var defaultChannel string 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. // InitSlack Initializes a slack client with the given token and sets the default channel.
func InitSlack(token string, channel string, username string) { 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 defaultChannel = channel
defaultUsername = username defaultUsername = username
} }
@ -65,7 +79,13 @@ func sendToSlack(channel, username, message string) error {
err = errors.Err("no slack token provided") err = errors.Err("no slack token provided")
} else { } else {
log.Debugln("slack: " + channel + ": " + message) log.Debugln("slack: " + channel + ": " + message)
_, _, err = slackApi.PostMessage(channel, slack.MsgOptionText(message, false), slack.MsgOptionUsername(username)) 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 { if err != nil {

53
extras/util/strings.go Normal file
View 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)
}

94
go.mod
View file

@ -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 ( require (
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf // indirect github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32
github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a 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.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 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
)
require (
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/davecgh/go-spew v1.1.0 github.com/google/go-cmp v0.5.9 // indirect
github.com/go-errors/errors v1.0.1 github.com/gorilla/websocket v1.4.2 // indirect
github.com/go-ini/ini v1.38.2 github.com/onsi/gomega v1.7.0 // indirect
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/golang/protobuf v1.3.0 github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect golang.org/x/sys v0.6.0 // indirect
github.com/gorilla/context v1.1.1 // indirect google.golang.org/appengine v1.6.7 // indirect
github.com/gorilla/mux v1.6.2 google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
github.com/gorilla/rpc v1.1.0 google.golang.org/protobuf v1.28.1 // indirect
github.com/gorilla/websocket v1.2.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04
github.com/lbryio/types v0.0.0-20190304154608-a6f2fd6d5f38
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
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/onsi/gomega v1.4.3 // indirect
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 v0.0.0-20180523074243-ea8897e79973
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 // indirect
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-20180608092829-8ac0e0d97ce4
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd
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/airbrake/gobrake.v2 v2.0.9 // indirect
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
gopkg.in/ini.v1 v1.41.0 // indirect
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79
) )

230
go.sum
View file

@ -1,136 +1,150 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a h1:fuEu3WaOzatbWFGlCa2e/TL/GR397Da8QSKNKrDRa3s=
github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= 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/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016 h1:BsZAJgCuMsoFZMZNyj7Lyt6sS8anDhedVrAMCOyPMIo= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 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 h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
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 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 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.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible h1:sUy/in/P6askYr16XJgTKq/0SZhiWsdg4WZGaLsGQkM= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/rpc v1.1.0 h1:marKfvVP0Gpd/jHlVBKCQ8RAoUPdX7K1Nuh6l1BNh7A= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/rpc v1.1.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c h1:BhdcWGsuKif/XoSZnqVGNqJ1iEmH0czWR5upj+AuR8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lbryio/types v0.0.0-20190304154608-a6f2fd6d5f38 h1:78rErXiBhMjs7vh4Ke3ZMqQjf+pnQeNrkXYmQ8L6aKE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lbryio/types v0.0.0-20190304154608-a6f2fd6d5f38/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19 h1:/zWD8dVIl7bV1TdJWqPqy9tpqixzX2Qxgit48h3hQcY=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= github.com/lbryio/ozzo-validation v3.0.3-0.20170512160344-202201e212ec+incompatible h1:OH/jgRO/2lQ73n7PgtK/CvLZ0dwAVr5G5s635+YfUA4=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns= github.com/lbryio/ozzo-validation v3.0.3-0.20170512160344-202201e212ec+incompatible/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= 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 h1:mG83tLXWSRdcXMWfkoumVwhcCbf3jHF9QKv/m37BkM0=
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5/go.mod h1:H0aPCWffGOaDcjkw1iB7W9DVLp6GXmfcJY/7YZCWPA4= 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 v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 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/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561/go.mod h1:lvjGftC8oe7XPtyrOidaMi0rp5B9+XY/ZRUynGnuaxQ= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930 h1:pSgp2x9zCkCjb8rxXFNpGE8hDIrt+UXW7jUQ5fbTlzM= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973 h1:3AJZYTzw3gm3TNTt30x0CCKD7GOn2sdd50Hn35fQkGY= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d h1:tQo6hjclyv3RHUgZOl6iWb2Y44A/sN9bf9LAYfuioEg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4 h1:wviDUSmtheHRBfoY8B9U8ELl2USoXi2YFwdGdpIIkzI= github.com/ybbus/jsonrpc/v2 v2.1.7 h1:QjoXuZhkXZ3oLBkrONBe2avzFkYeYLorpeA+d8175XQ=
golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= github.com/ybbus/jsonrpc/v2 v2.1.7/go.mod h1:rIuG1+ORoiqocf9xs/v+ecaAVeo3zcZHQgInyKFMeg0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 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-20180314180146-1d60e4601c6f/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-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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f h1:FU37niK8AQ59mHcskRyQL7H0ErSeNh650vdcj8HqdSI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
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 h1:FpCr9V8wuOei4BAen+93HtVJ+XSi+KPbaPKm0Vj5R64=
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79/go.mod h1:gWkaRU7CoXpezCBWfWjm3999QqS+1pYPXGbqQCTMzo8= 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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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=

View file

@ -3,7 +3,7 @@ package lbrycrd
import ( import (
"encoding/hex" "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/btcd/chaincfg"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
@ -34,7 +34,7 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (btcutil.Address, e
if err == base58.ErrChecksum { if err == base58.ErrChecksum {
return nil, btcutil.ErrChecksumMismatch 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) { switch len(decoded) {

12
lbrycrd/address_test.go Normal file
View 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())
}

View file

@ -3,7 +3,7 @@ package lbrycrd_test
import ( import (
"testing" "testing"
"github.com/lbryio/lbry.go/lbrycrd" "github.com/lbryio/lbry.go/v2/lbrycrd"
) )
var claimIdTests = []struct { var claimIdTests = []struct {

40
lbrycrd/channel.go Normal file
View 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
View 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
}

View file

@ -1,15 +1,20 @@
package lbrycrd package lbrycrd
import ( import (
"encoding/hex"
"net/url" "net/url"
"os" "os"
"strconv" "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"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/go-ini/ini" "github.com/go-ini/ini"
) )
@ -28,10 +33,53 @@ var MainNetParams = chaincfg.Params{
PubKeyHashAddrID: 0x55, PubKeyHashAddrID: 0x55,
ScriptHashAddrID: 0x7a, ScriptHashAddrID: 0x7a,
PrivateKeyID: 0x1c, PrivateKeyID: 0x1c,
Bech32HRPSegwit: "not-used", // we don't have this (yet) Bech32HRPSegwit: "lbc",
GenesisHash: &GenesisHash, //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() { func init() {
// Register lbrycrd network // Register lbrycrd network
err := chaincfg.Register(&MainNetParams) err := chaincfg.Register(&MainNetParams)
@ -46,7 +94,7 @@ type Client struct {
} }
// New initializes a new Client // 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. // Connect to local bitcoin core RPC server using HTTP POST mode.
u, err := url.Parse(lbrycrdURL) u, err := url.Parse(lbrycrdURL)
@ -60,10 +108,16 @@ func New(lbrycrdURL string) (*Client, error) {
password, _ := u.User.Password() password, _ := u.User.Password()
chain := MainNetParams
if chainParams != nil {
chain = *chainParams
}
connCfg := &rpcclient.ConnConfig{ connCfg := &rpcclient.ConnConfig{
Host: u.Host, Host: u.Host,
User: u.User.Username(), User: u.User.Username(),
Pass: password, Pass: password,
Params: chain,
HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode
DisableTLS: true, // Bitcoin core does not provide TLS by default 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 // make sure lbrycrd is running and responsive
_, err = client.GetInfo() _, err = client.GetBlockChainInfo()
if err != nil { if err != nil {
return nil, errors.Err(err) return nil, errors.Err(err)
} }
@ -82,12 +136,12 @@ func New(lbrycrdURL string) (*Client, error) {
return &Client{client}, nil return &Client{client}, nil
} }
func NewWithDefaultURL() (*Client, error) { func NewWithDefaultURL(chainParams *chaincfg.Params) (*Client, error) {
url, err := getLbrycrdURLFromConfFile() url, err := getLbrycrdURLFromConfFile()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return New(url) return New(url, chainParams)
} }
var errInsufficientFunds = errors.Base("insufficient funds") 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) 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 != nil {
if err.Error() == "-6: Insufficient funds" { if err.Error() == "-6: Insufficient funds" {
err = errors.Err(errInsufficientFunds) err = errors.Err(errInsufficientFunds)
@ -114,30 +168,6 @@ func (c *Client) SimpleSend(toAddress string, amount float64) (*chainhash.Hash,
return hash, nil 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) { func getLbrycrdURLFromConfFile() (string, error) {
if os.Getenv("HOME") == "" { if os.Getenv("HOME") == "" {
return "", errors.Err("no $HOME var found") return "", errors.Err("no $HOME var found")
@ -179,3 +209,179 @@ func getLbrycrdURLFromConfFile() (string, error) {
return "rpc://" + userpass + host + ":" + port, nil 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
View 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
View 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()
}

View file

@ -6,7 +6,7 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/lbryio/lbry.go/claim" "github.com/lbryio/lbry.go/v2/claim"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View file

@ -2,6 +2,8 @@
lbry.go is a set of tools and projects implemented in Golang. See each subfolder for more details 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) [![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. This project uses Go modules. Make sure you have Go 1.11+ installed.
@ -16,19 +18,26 @@ This project uses Go modules. Make sure you have Go 1.11+ installed.
## Contributing ## Contributing
Contributions to this project are welcome, encouraged, and compensated. For more details, see [lbry.io/faq/contributing](https://lbry.io/faq/contributing) Contributions to this project are welcome, encouraged, and compensated. For more details, see [lbry.com/faq/contributing](https://lbry.com/faq/contributing)
Make sure you `go fmt` your code before submitting PRs. Make sure you `go fmt` your code before submitting PRs.
## Versioning
This codebase follows the semver standards. You can find the releases [here](https://github.com/lbryio/lbry.go/releases)
You can include the libraries by running `go get github.com/lbryio/lbry.go@latest`.
You can lock (and you should) to a version whenever you make use of this set of libraries.
## License ## License
This project is MIT licensed. For the full license, see [LICENSE](LICENSE). This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
## Security ## Security
We take security seriously. Please contact security@lbry.io regarding any issues you may encounter. 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 ## Contact
The primary contact for this project is [@lyoshenka](https://github.com/lyoshenka) (grin@lbry.io) The primary contact for this project is [@lyoshenka](https://github.com/lyoshenka) (grin@lbry.com)

4
schema/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.idea/
lbryschema-cli
lbryschema-python-binding.h
lbryschema-python-binding.so

9
schema/.travis.yml Normal file
View file

@ -0,0 +1,9 @@
os: linux
dist: trusty
language: go
go:
- 1.15.x
script:
- ./build_and_test.sh

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

View file

@ -0,0 +1 @@
package base58

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

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

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

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

View 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
View 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
View file

@ -0,0 +1,5 @@
#!/bin/bash
set -euxo pipefail
./build.sh
./test.sh

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

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

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

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

View 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
View file

@ -0,0 +1 @@
go test ./... -v

7
scripts/deploy.sh Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
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)"
curl -sL https://git.io/goreleaser | bash

5
scripts/release.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
go install github.com/caarlos0/svu@latest
git tag `svu "$1"`
git push --tags

View file

@ -8,10 +8,14 @@ import (
"encoding/hex" "encoding/hex"
"strconv" "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 type Blob []byte
@ -31,7 +35,7 @@ func (b Blob) Hash() []byte {
return hashBytes[:] 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 { func (b Blob) HashHex() string {
return hex.EncodeToString(b.Hash()) return hex.EncodeToString(b.Hash())
} }

View file

@ -4,7 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "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/ // inspired by https://blog.gopheracademy.com/advent-2016/advanced-encoding-decoding/

View file

@ -8,6 +8,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"strconv" "strconv"
"strings"
) )
const streamTypeLBRYFile = "lbryfile" const streamTypeLBRYFile = "lbryfile"
@ -44,10 +45,39 @@ type SDBlob struct {
StreamHash []byte `json:"-"` 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 // ToBlob converts the SDBlob to a normal data Blob
func (s SDBlob) ToBlob() (Blob, error) { func (s SDBlob) ToBlob() Blob {
b, err := json.Marshal(s) jsonSD, err := json.Marshal(s)
return Blob(b), err 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 // 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) 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 // addBlob adds the blob's info to stream
func (s *SDBlob) addBlob(b Blob, iv []byte) { func (s *SDBlob) addBlob(b Blob, iv []byte) {
if len(iv) == 0 { if len(iv) == 0 {

View file

@ -2,10 +2,12 @@ package stream
import ( import (
"bytes" "bytes"
"crypto/sha512"
"hash"
"io"
"math" "math"
"strings"
"github.com/lbryio/lbry.go/extras/errors" "github.com/lbryio/lbry.go/v2/extras/errors"
) )
type Stream []Blob 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 // -1 to leave room for padding, since there must be at least one byte of pkcs7 padding
const maxBlobDataSize = MaxBlobSize - 1 const maxBlobDataSize = MaxBlobSize - 1
// New creates a new Stream from a byte slice // New creates a new Stream from a stream of bytes.
func New(data []byte) (Stream, error) { func New(src io.Reader) (Stream, error) {
key := randIV() return NewEncoder(src).Stream()
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
} }
// 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) { 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 { if len(s) < 2 {
return nil, errors.Err("stream must be at least 2 blobs long") // sd blob and content blob 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 return file, nil
} }
//numContentBlobs returns the number of content blobs required to store the data // Encoder reads bytes from a source and returns blobs of the stream
func numContentBlobs(data []byte) int { type Encoder struct {
return int(math.Ceil(float64(len(data)) / float64(maxBlobDataSize))) // 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
} }

View file

@ -2,26 +2,31 @@ package stream
import ( import (
"bytes" "bytes"
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"encoding/hex" "encoding/hex"
"io"
"testing" "testing"
"github.com/lbryio/lbry.go/v2/extras/errors"
) )
func TestStreamToFile(t *testing.T) { var testdataBlobHashes = []string{
blobHashes := []string{ "1bf7d39c45d1a38ffa74bff179bf7f67d400ff57fa0b5a0308963f08d01712b3079530a8c188e8c89d9b390c6ee06f05", // sd hash
"1bf7d39c45d1a38ffa74bff179bf7f67d400ff57fa0b5a0308963f08d01712b3079530a8c188e8c89d9b390c6ee06f05", // sd hash "a2f1841bb9c5f3b583ac3b8c07ee1a5bf9cc48923721c30d5ca6318615776c284e8936d72fa4db7fdda2e4e9598b1e6c",
"a2f1841bb9c5f3b583ac3b8c07ee1a5bf9cc48923721c30d5ca6318615776c284e8936d72fa4db7fdda2e4e9598b1e6c", "0c9675ad7f40f29dcd41883ed9cf7e145bbb13976d9b83ab9354f4f61a87f0f7771a56724c2aa7a5ab43c68d7942e5cb",
"0c9675ad7f40f29dcd41883ed9cf7e145bbb13976d9b83ab9354f4f61a87f0f7771a56724c2aa7a5ab43c68d7942e5cb", "a4d07d442b9907036c75b6c92db316a8b8428733bf5ec976627a48a7c862bf84db33075d54125a7c0b297bd2dc445f1c",
"a4d07d442b9907036c75b6c92db316a8b8428733bf5ec976627a48a7c862bf84db33075d54125a7c0b297bd2dc445f1c", "dcd2093f4a3eca9f6dd59d785d0bef068fee788481986aa894cf72ed4d992c0ff9d19d1743525de2f5c3c62f5ede1c58",
"dcd2093f4a3eca9f6dd59d785d0bef068fee788481986aa894cf72ed4d992c0ff9d19d1743525de2f5c3c62f5ede1c58", }
}
stream := make(Stream, len(blobHashes)) func TestStreamToFile(t *testing.T) {
for i, hash := range blobHashes { stream := make(Stream, len(testdataBlobHashes))
for i, hash := range testdataBlobHashes {
stream[i] = testdata(t, hash) stream[i] = testdata(t, hash)
} }
data, err := stream.Data() data, err := stream.Decode()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -33,6 +38,8 @@ func TestStreamToFile(t *testing.T) {
t.Errorf("file length mismatch. got %d, expected %d", actualLen, expectedLen) t.Errorf("file length mismatch. got %d, expected %d", actualLen, expectedLen)
} }
expectedFileHash := sha512.Sum384(data)
expectedSha256 := unhex(t, "51e4d03bd6d69ea17d1be3ce01fdffa44ffe053f2dbce8d42a50283b2890fea2") expectedSha256 := unhex(t, "51e4d03bd6d69ea17d1be3ce01fdffa44ffe053f2dbce8d42a50283b2890fea2")
actualSha256 := sha256.Sum256(data) actualSha256 := sha256.Sum256(data)
@ -46,22 +53,150 @@ func TestStreamToFile(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
newStream, err := Reconstruct(data, *sdBlob) enc := NewEncoderFromSD(bytes.NewBuffer(data), sdBlob)
if err != nil { newStream, err := enc.Stream()
t.Fatal(err)
if len(newStream) != len(testdataBlobHashes) {
t.Fatalf("stream length mismatch. got %d blobs, expected %d", len(newStream), len(testdataBlobHashes))
} }
if len(newStream) != len(blobHashes) { if enc.SourceLen() != expectedLen {
t.Fatalf("stream length mismatch. got %d blobs, expected %d", len(newStream), len(blobHashes)) 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 { if newStream[i].HashHex() != hash {
t.Errorf("blob %d hash mismatch. got %s, expected %s", i, 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) { func TestNew(t *testing.T) {
t.Skip("TODO: test new stream creation and decryption") t.Skip("TODO: test new stream creation and decryption")
} }

347
url/url.go Normal file
View 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
}