Compare commits

...

199 Commits

Author SHA1 Message Date
wltsmrz
fd0dc3b330 Merge pull request #108 from shekenahglory/develop
[FIX] vault client: rippleTxt test request diverted to offline mock
2014-06-23 13:45:41 -07:00
Matthew Fettig
3ebcadfad4 [FIX] vault client: rippleTxt test request diverted to offline mock 2014-06-23 13:44:16 -07:00
wltsmrz
f963d266a2 Merge pull request #107 from geertweening/update_history
[DOC] update changelog with currency changes
2014-06-23 13:23:04 -07:00
shekenahglory
309957a6cf Update HISTORY.md 2014-06-23 13:16:19 -07:00
Geert Weening
47094f84d7 [DOC] update changelog with currency changes 2014-06-23 12:01:13 -07:00
wltsmrz
68c86bf672 Merge pull request #106 from shekenahglory/develop
[FIX] vault client: set SJCL test entropy random string to base64
2014-06-23 11:52:41 -07:00
Matthew Fettig
e6782f4563 [FIX] vault client: set SJCL test entropy random string to base64 2014-06-23 11:30:06 -07:00
wltsmrz
1b2b19381b Merge pull request #103 from shekenahglory/develop
[FEATURE] vault client: updateProfile and deleteBlob
2014-06-23 11:09:30 -07:00
wltsmrz
d495f397b0 Bump version 2014-06-23 10:58:48 -07:00
wltsmrz
f1a58de348 Update HISTORY.md 2014-06-23 10:58:36 -07:00
Matthew Fettig
6ae186951b [FEATURE] vault client: updateProfile and deleteBlob 2014-06-19 15:58:38 -07:00
wltsmrz
8d77ff9af7 Merge pull request #102 from shekenahglory/develop
Save encrypted blob decrypt key at login if missing
2014-06-18 21:35:16 -07:00
Matthew Fettig
eeb5e22e9a [FIX] blob: catch missing encrypted blob decrypt key 2014-06-18 17:16:40 -07:00
Matthew Fettig
fc5284cc88 [CHORE] RippleTxt: add tests for new functions 2014-06-18 16:40:55 -07:00
Matthew Fettig
6a7eb132bd [FEATURE] RippleTxt: parse ripple.txt for currencies 2014-06-18 16:28:18 -07:00
Matthew Fettig
f8519584d2 [CHORE] RippleTxt and AuthInfo changed from classes to objects
fix vault test entropy issue
refactor missing feilds on login
2014-06-18 12:36:33 -07:00
Matthew Fettig
58b307411e [FIX] vault client: account registration using lowercase version of username
also added username validation with rules from ripple trade client
2014-06-17 14:12:11 -07:00
Matthew Fettig
c7dd3cc70e [FIX] vault-test: increase entropy from randomBytes
When running vault-test.js alone, the entropy was insufficient
for the seed generator.  Entropy must be added elsewhere when
the full test suite is run.
2014-06-17 13:29:27 -07:00
Matthew Fettig
dc62dbe022 [FEATURE] vault client: create encrypted blob decrypt key if missing
if the key is missing, upon login the key will be automatically
generated and saved to the blobvault.
2014-06-17 13:20:28 -07:00
Vahe Hovhannisyan
e50ddd6237 [FIX] Blobvault: Include domain on account creation call
Blobvault now requires a domain field to be passed on the account creation request.
2014-06-16 15:43:34 -07:00
wltsmrz
6a55dbc55e Add build script 2014-06-16 13:02:52 -07:00
wltsmrz
4e7cf460ec Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-06-16 12:57:02 -07:00
wltsmrz
87dd8d535a Remove dedupe plugin from minified webpack build 2014-06-16 12:56:28 -07:00
wltsmrz
ce6010ecd3 Merge pull request #101 from shekenahglory/develop
Offline vault client tests and improved coverage
2014-06-14 17:13:02 -07:00
Matthew Fettig
ac5f146187 [CHORE] change randomBytes to 256 for vault test 2014-06-14 09:13:14 -07:00
Matthew Fettig
14b2697a52 [CHORE] use strict equal on vault tests 2014-06-14 09:01:35 -07:00
Matthew Fettig
a4078e10e9 [CHORE] vault client test for blob creation 2014-06-13 14:54:41 -07:00
Matthew Fettig
06796aa7c0 Merge branch 'feature/nock' into develop 2014-06-13 14:36:42 -07:00
Matthew Fettig
62d4be0185 [CHORE] offline tests for vault client 2014-06-13 14:35:37 -07:00
Matthew Fettig
2e0536ea2d bug fixes and documentation 2014-06-13 14:35:01 -07:00
Matthew Fettig
94c7408b5b [FEATURE] offline vault client tests 2014-06-12 17:46:34 -07:00
wltsmrz
1186b4a314 Fix fractional transaction fee 2014-06-12 03:06:23 -07:00
wltsmrz
1cccf01bf2 Merge pull request #99 from sublimator/develop
Test we don't compute floating point median Fee
2014-06-12 02:58:40 -07:00
sublimator
8f7cda3c2c testify 2014-06-12 16:22:19 +07:00
wltsmrz
c95ac13946 Merge pull request #98 from ripple/tests
Improve test coverage
2014-06-12 00:33:14 -07:00
wltsmrz
ee1ce36045 Use URL rather than hostname for setServer lookup 2014-06-12 00:29:23 -07:00
wltsmrz
a4f22d8b42 Improve transaction-test coverage 2014-06-12 00:20:56 -07:00
wltsmrz
2afce7cf54 Improve server-test coverage 2014-06-11 23:19:50 -07:00
wltsmrz
6150721951 Improve request-test coverage 2014-06-11 22:24:21 -07:00
wltsmrz
61b3ef0205 Merge pull request #96 from ripple/tests
Add server and transaction test
2014-06-11 21:44:28 -07:00
wltsmrz
06108ffee3 Fix server test 2014-06-11 21:40:41 -07:00
wltsmrz
726b309085 [THING] Add transaction test 2014-06-11 21:23:27 -07:00
wltsmrz
ff14b55ea5 Merge pull request #94 from shekenahglory/develop
Blob recover, change password, and refactoring
2014-06-11 16:32:25 -07:00
Matthew Fettig
bb82eb9219 [BUG] fix bug on update blob keys 2014-06-11 16:10:24 -07:00
Matthew Fettig
abc43f80e0 Merge vault client refactoring 2014-06-11 15:54:58 -07:00
Matthew Fettig
f6343dc1d1 [CHORE] refactor vault client functions 2014-06-11 15:51:59 -07:00
Matthew Fettig
182e1863f4 [FEATURE] recover blob and change password 2014-06-11 09:26:03 -07:00
Vahe Hovhannisyan
0a8d4ad587 Blobvault: Ripple name change. 2014-06-10 16:14:20 -07:00
wltsmrz
48e49ac42b Fix server test 2014-06-10 15:15:56 -07:00
wltsmrz
1579d58edf [THING] Add server test 2014-06-09 13:03:02 -07:00
wltsmrz
61cb21188d Merge pull request #93 from geertweening/fix/amount_regex
[FIX] decimal precision amount parsing
2014-06-05 14:50:34 -07:00
Geert Weening
49d50c02d4 [FIX] decimal precision amount parsing
- add non-grouping operator to regex
- add tests to cover the problematic cases
2014-06-05 14:14:50 -07:00
wltsmrz
b7f7e6dc60 Merge pull request #92 from ripple/tests
Update tests
2014-06-04 18:10:13 -07:00
wltsmrz
e49bb4e527 Update tests 2014-06-04 18:04:57 -07:00
wltsmrz
44a9724b2a Merge pull request #91 from shekenahglory/develop
Necessary updates for vault client integration in ripple trade
2014-06-04 17:41:29 -07:00
Matthew Fettig
7af4a376a2 [CHORE] lint cleanup 2014-06-04 15:14:05 -07:00
Matthew Fettig
77e69efe19 [FEATURE] blobvault: resend email verification 2014-06-04 15:09:22 -07:00
Matthew Fettig
da8061ed52 [FEATURE] expose AuthInfo and RippleTxt modules in ripple class 2014-06-04 15:08:08 -07:00
Matthew Fettig
90d65573b5 Merge remote-tracking branch 'upstream/develop' into develop 2014-06-04 11:51:52 -07:00
wltsmrz
838180a498 Merge pull request #89 from ripple/multi-server
Multi-server update
2014-06-04 11:47:02 -07:00
Matthew Fettig
5ebab2fe28 [CHORE] refactored identity functions necessary for ripple client integration 2014-06-04 10:45:05 -07:00
Matthew Fettig
0ff0004d42 Merge remote-tracking branch 'upstream/develop' into develop 2014-06-04 10:03:17 -07:00
wltsmrz
b931b92773 Merge pull request #90 from geertweening/feature/iso_code
[FEATURE] add getter for a Currency's iso_code
2014-06-03 16:50:12 -07:00
wltsmrz
ad08e20085 Cleanup 2014-06-03 16:44:36 -07:00
wltsmrz
c1c18c465d Use median rather than average transaction fee 2014-06-03 16:36:18 -07:00
Geert Weening
a71fc07ba5 [FEATURE] add getter for a Currency's iso_code 2014-06-03 15:18:45 -07:00
Matthew Fettig
89c4839fa5 [DEBUG] multiple bug fixes with blob creation 2014-06-03 14:06:15 -07:00
wltsmrz
41ee43740e Merge pull request #88 from justmoon/feature/generate_ledger_entry_indexes
Generate ledger entry indexes
2014-06-03 13:41:08 -07:00
wltsmrz
179c215b15 Uncomment reconnect logic 2014-06-03 13:36:06 -07:00
Stefan Thomas
9100e8ecc0 [FEATURE] Add ability to calculate ledger entry keys. 2014-06-04 00:12:10 +04:00
Stefan Thomas
fb213e5818 [CHORE] Move HTTP request signing functionality into its own class. 2014-06-04 00:12:06 +04:00
Matthew Fettig
ac12e3fb5c Merge remote-tracking branch 'upstream/develop' into develop 2014-06-03 13:03:20 -07:00
wltsmrz
caa78b11dd Merge pull request #87 from geertweening/feature/numeric_currency_code
[FEATURE] support numeric currency codes
2014-06-03 12:21:24 -07:00
Geert Weening
61586a4185 [FEATURE] support numeric currency codes
ISO 4217 states support for both alphabetic and numeric codes. rippled adheres to the ISO 4217 as stated in the Currency_Format on the wiki (https://ripple.com/wiki/Currency_Format) and there are trustlines out there with numeric currency codes.

The three-digit numeric code is useful when currency codes need to be understood in countries that do not use Latin scripts and for computerised systems. Where possible the 3 digit numeric code is the same as the numeric country code.
2014-06-02 17:14:22 -07:00
wltsmrz
a7df5248c9 Broadcast transaction submission requests, use average server fee 2014-06-02 17:11:46 -07:00
wltsmrz
cf53ec9da8 Merge pull request #83 from geertweening/feature-demurrage
Interest bearing currency parsing support
2014-06-02 12:52:15 -07:00
Matthew Fettig
46e966fb7f Merge remote-tracking branch 'upstream/develop' into develop 2014-06-02 09:17:14 -07:00
Matthew Fettig
01e38ed4ca [BUG] merge IE11 signature fix 2014-06-02 09:16:48 -07:00
wltsmrz
01459061ee Merge pull request #86 from shekenahglory/develop
Identity functions for blob vault storage
2014-05-30 15:55:42 -07:00
Geert Weening
3ef105e077 [FEATURE] support full_name in to_human() and to_json() 2014-05-30 15:10:07 -07:00
Geert Weening
decebe3d2e [FEATURE] allow numbers, hyphens and spaces in full currency 2014-05-30 15:10:07 -07:00
Geert Weening
2a832777a7 [TEST] add more from_number tests 2014-05-30 15:10:07 -07:00
Geert Weening
6a718c4384 [FIX] parse_number correctly setting _is_negative 2014-05-30 15:10:07 -07:00
Geert Weening
a1face76b7 [FIX] parse_number should create a non-native currency
"1" just means "some issuer, don't care which"
2014-05-30 15:10:07 -07:00
Geert Weening
8104f71162 [FIX] Currency by number default type
should be a non-interest bearing currency
2014-05-30 15:10:07 -07:00
Geert Weening
1ce2a517aa [TEST] update amount test for interest bearing currencies 2014-05-30 15:10:07 -07:00
Geert Weening
53c7836a7a [TEST] test human readable Currency parsing 2014-05-30 15:10:07 -07:00
Geert Weening
160b6e8a51 [FEATURE] human readable demurrage support
- human readable string to Currency
- Currency to human readable string
2014-05-30 15:10:07 -07:00
Geert Weening
b1f6284813 [CHORE] add IEEE-754 Double Precision float support
for an 8 byte representation of floats
2014-05-30 15:10:07 -07:00
Matthew Fettig
d104ebb6f5 [BUG] fix bug with nationalID types 2014-05-30 14:47:57 -07:00
Matthew Fettig
017713c435 [CHORE] merge upstream changes 2014-05-30 14:35:01 -07:00
wltsmrz
644ca2b472 Fix authinfo test 2014-05-30 13:45:14 -07:00
wltsmrz
453ff91065 Comma should be semicolon, woopsy 2014-05-30 13:30:48 -07:00
Matthew Fettig
8b79ec0e5a [DOCS] added Vault Client Documentation 2014-05-30 13:01:22 -07:00
Matthew Fettig
95b7858c8f [CHORE] require specific entity types 2014-05-30 13:00:41 -07:00
Matthew Fettig
5ef7e5462e Merge branch 'feature/best-practices' into develop 2014-05-30 11:54:41 -07:00
Matthew Fettig
54a2655bc2 [CHORE] gulp lint cleanup 2014-05-30 11:53:05 -07:00
Matthew Fettig
785a066ebb [CHORE] Crypt exported by name 2014-05-30 11:10:25 -07:00
Matthew Fettig
fa60b182a3 [CHORE] RippleTxt and AuthInfo exported by name 2014-05-30 10:57:00 -07:00
Matthew Fettig
dbc965de3c [CHORE] VaultClient exported by name 2014-05-30 10:52:57 -07:00
Matthew Fettig
77814b791d Merge branch 'feature/identity' into develop 2014-05-30 10:46:19 -07:00
Matthew Fettig
ddf12d43c3 [CHORE] BlobClient exported by name 2014-05-30 10:39:41 -07:00
Matthew Fettig
f5b59b4268 [FEATURE] tests for identity functions 2014-05-30 10:13:33 -07:00
Matthew Fettig
58e14f3bb3 [CHORE] limit identity to defined fields 2014-05-30 10:03:49 -07:00
wltsmrz
1c02166662 Fix server url formatting 2014-05-30 00:50:06 -07:00
Matthew Fettig
0e5c29269f [FEATURE] identity unset, getAll, getFullAddress 2014-05-29 17:52:27 -07:00
wltsmrz
9a349a3d55 Begin work on offline vault client tests 2014-05-29 14:33:53 -07:00
Matthew Fettig
d5e01adbf9 [FEATURE] identity get and set 2014-05-29 13:57:58 -07:00
wltsmrz
61bc01ae12 Cleanup 2014-05-29 06:46:54 -07:00
wltsmrz
c3568de8b3 Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-05-28 18:23:11 -07:00
wltsmrz
eca5ac7611 Cleanup 2014-05-28 18:22:54 -07:00
wltsmrz
a8e0cb2e0b Merge pull request #82 from emoitzi/develop
[FIX] remote.getPendingTransactions sets _clientID now correctly
2014-05-28 12:38:31 -07:00
Matthew Fettig
799bb5faeb [CHORE] stubbed functions 2014-05-28 10:13:37 -07:00
Ewald Moitzi
0b5c0722e0 removed .only from remote testcase for pending transactions 2014-05-27 12:43:56 +02:00
Ewald Moitzi
3964e4522e [FIX] remote.getPendingTransactions sets _clientID now correctly
- added testcase for it
2014-05-27 12:27:28 +02:00
wltsmrz
bdb299e085 Merge branch 'multi-server' into develop 2014-05-23 12:27:09 -07:00
wltsmrz
349ca81cc9 Merge pull request #80 from geertweening/amount-regex
Amount regex
2014-05-21 10:57:46 -07:00
Geert Weening
77a323aaae [CHJORE] gitignore intellij files 2014-05-21 09:40:50 -07:00
Geert Weening
8be08b5e73 [DOC] regex breakdown for human parseble amount 2014-05-21 09:40:40 -07:00
Geert Weening
b53b05496d [CHORE] remove non-capturing group (?:)
it doesn't affect the matching, seems superfluous
2014-05-21 09:39:40 -07:00
wltsmrz
bd2dba7f15 Merge pull request #79 from shekenahglory/develop
Ripple Vault Client Integration
2014-05-21 00:43:45 -07:00
Matthew Fettig
81e0e2672d [CHORE] new example data 2014-05-20 16:47:15 -07:00
Matthew Fettig
bd65c6e6d0 Merge branch 'feature/vault-client' into develop 2014-05-20 15:57:21 -07:00
Matthew Fettig
396d7b07d8 [DOC] blob comments 2014-05-20 15:56:07 -07:00
Matthew Fettig
55184162d1 [DOC] crypt comments 2014-05-20 15:40:27 -07:00
Matthew Fettig
beffd0864a [FEATURE] register blob 2014-05-20 15:10:16 -07:00
Matthew Fettig
5ba7c31e4c [DOC] vault client comments 2014-05-20 13:40:05 -07:00
Matthew Fettig
b26129db72 [FEATURE] vault, blob, and crypt module 2014-05-20 13:01:51 -07:00
Matthew Fettig
1693a57845 [CHORE] update comments 2014-05-19 16:50:02 -07:00
Matthew Fettig
7ea1ba168d [FEATURE] authinfo module 2014-05-19 16:38:51 -07:00
Matthew Fettig
7fe530e82c [FEATURE] ripple.txt client 2014-05-19 16:00:53 -07:00
Matthew Fettig
7e4ae26b8f [CHORE] include superagent, vault-test 2014-05-19 15:25:53 -07:00
wltsmrz
963e1d58cb Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-05-19 12:08:08 -07:00
wltsmrz
cc6f683590 Fix merge conflicts 2014-05-19 12:07:24 -07:00
Nicholas Dudfield
da3af124ba Update SerializedObject.from_json tests 2014-05-19 15:04:24 +07:00
wltsmrz
b342cf1edf Rank servers in real-time (first pass), add JSDOc to Remote, cleanup 2014-05-18 08:39:47 -07:00
Nicholas Dudfield
a1989b3931 Tweak to parse_options 2014-05-18 17:37:54 +07:00
Nicholas Dudfield
80bdce970a Add binary <--> json encoding integration tests
* Add full json ledger dumps of ledgers 38129 and 40000 to test/fixtures
* Use `Ledger` to calculate account and transaction hashes and verify
  against dumps
2014-05-18 17:33:47 +07:00
wltsmrz
7cf80a468d Merge pull request #77 from justmoon/feature/coveralls
Switch to Istanbul for code coverage
2014-05-17 06:13:48 -07:00
Stefan Thomas
bee632e1e2 Re-enable Travis email notifications. 2014-05-17 10:00:01 +02:00
Stefan Thomas
7901e12b0a Switch to Istanbul for code coverage. Add coverage to CI via coveralls. 2014-05-17 09:59:20 +02:00
wltsmrz
8979a3cf02 Merge branch account-ids 2014-05-15 07:25:16 -07:00
wltsmrz
9025119f8d Typo, check that request callback is only called once 2014-05-15 07:00:04 -07:00
wltsmrz
fdaa63c132 Use mocha specification reporter 2014-05-15 03:02:39 -07:00
wltsmrz
9e825e927c Use Gulp build system 2014-05-15 02:59:06 -07:00
wltsmrz
4929d63073 Remove sinon require 2014-05-15 02:56:07 -07:00
wltsmrz
5b0dd33fa8 Lint 2014-05-15 00:58:09 -07:00
wltsmrz
5203a1e868 Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-05-14 01:29:38 -07:00
Stefan Thomas
27645c234a [CHORE] Currency: JavaScript has Math.exp for e^x, so we should use it. 2014-05-11 01:54:15 -07:00
Nicholas Dudfield
5280d994a2 Fix *U*Int64 parsing, add hardcore mode to ledger verifier. 2014-05-06 14:54:22 +07:00
Nicholas Dudfield
12f43a5334 Remove all traces of (now) unused tag_segment 2014-05-06 14:16:53 +07:00
Nicholas Dudfield
4f0399180c Reinstate disabled test (Thanks to encoded reminder) 2014-05-06 13:56:20 +07:00
Nicholas Dudfield
ea11d34254 Less slicing and dicing of tag_segment/indexes 2014-05-06 13:47:37 +07:00
Nicholas Dudfield
a222f2be98 Updates to allow verify_ledger_json.js to calculate account_state hash 2014-05-06 13:34:01 +07:00
wltsmrz
a48a25e236 Optional account_tx binary parsing 2014-05-05 15:05:02 -07:00
Stefan Thomas
473d8a8d8c [CHORE] Bump version to 0.7.36 2014-05-02 14:06:53 -07:00
Stefan Thomas
41ea820ae0 [FEATURE] Transaction: Allow canonical signing to be disabled via config. 2014-05-02 14:06:53 -07:00
wltsmrz
0558ad689a Async transaction sign 2014-05-02 12:45:26 -07:00
wltsmrz
3199aa438a Allow remote signing - broken options 2014-05-02 12:41:52 -07:00
Stefan Thomas
c3f630c27f Merge pull request #72 from emschwartz/message-signing
[FEATURE] New Message class for sigs on arbitrary data
2014-05-02 10:59:25 -07:00
Evan Schwartz
cf3a21a712 [CHORE] Merged PubKeyValidator into Account class 2014-05-01 20:28:55 -07:00
Evan Schwartz
d8504a3001 [CHORE] Changed variable name to make Stefan happier 2014-05-01 19:37:59 -07:00
Evan Schwartz
a2b07d5edd [FIX] Handling public key validation for unfunded accounts 2014-05-01 17:22:20 -07:00
Evan Schwartz
13a6a2c335 [CHORE] Added pre-built sjcl with additional functions included 2014-05-01 16:29:23 -07:00
Evan Schwartz
e19be192bd [FIX] Point coordinates should be converted to psuedo mersenne primes 2014-05-01 13:14:55 -07:00
Evan Schwartz
c32216c9e5 [CHORE] Added account param to signing functions 2014-05-01 12:08:14 -07:00
Evan Schwartz
904082a86c [FEATURE] New Message class for sigs on arbitrary data
This includes supporting files that can sign arbitrary data
with a signature that enables public key recovery. It also
includes the PublicKeyValidator class that can verify whether
a given public key is active for an account by looking in its
AccountRoot.
2014-04-30 18:41:18 -07:00
Evan Schwartz
f56a20d697 [FIX] Replaced Account.is_valid() with Account.isValid() 2014-04-30 18:41:18 -07:00
Evan Schwartz
8275e036c9 [CHORE] Modified functions not to overwrite entire prototype 2014-04-30 18:38:36 -07:00
wltsmrz
903e480130 Fix for missing transaction.remote 2014-04-25 01:38:01 -07:00
wltsmrz
30fd0e7bff Finalize before setting final transaction state 2014-04-25 00:08:26 -07:00
Stefan Thomas
fbdef6eea0 [BUG] UInt#parse_number should support zero. Add tests. 2014-04-24 09:01:18 -07:00
wltsmrz
5a04ce9629 Use binary sequence increment 2014-04-23 14:42:29 -07:00
wltsmrz
693e2aaae7 Decrement transaction sequence if transaction.complete fails 2014-04-23 14:39:30 -07:00
wltsmrz
43deeaf5fb Remove logging 2014-04-23 14:20:41 -07:00
wltsmrz
cbba7727f2 Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-04-23 14:19:19 -07:00
wltsmrz
52e1665e72 Fixes for per-transaction defined secret and offline errors 2014-04-23 14:19:02 -07:00
sublimator
66ea770287 Fix README
1000th
2014-04-23 18:23:25 +07:00
wltsmrz
18efa5d742 Emit an error on invalid secret, cleanup 2014-04-23 03:56:36 -07:00
wltsmrz
802212bbdc Merge pull request #71 from emschwartz/develop
[FEATURE] Added setRegularKey transaction and more accountSet flags
2014-04-22 19:06:42 -07:00
Evan Schwartz
7f59fb917c [FEATURE] Added setRegularKey transaction and more accountSet flags 2014-04-22 14:57:13 -07:00
wltsmrz
14f409ff56 Properly convert JS time to Ripple time in OfferCreate transactions 2014-04-18 23:08:19 -07:00
wltsmrz
8ffd0b13a3 Cleanup 2014-04-17 15:19:08 -07:00
wltsmrz
969873441e Recognize account option as equivalent to source in transaction construction 2014-04-15 12:47:21 -07:00
wltsmrz
282ac6d8ab Fix transaction constructor 2014-04-15 12:38:57 -07:00
wltsmrz
1e3c96b14f Fix transaction finalize 2014-04-15 12:27:33 -07:00
wltsmrz
b14fab8aa7 Check remote exists in transaction.complete 2014-04-10 23:29:16 -07:00
Stefan Thomas
be33b1be60 [BUG] Don't set canonical flag when remote signing. 2014-04-10 08:16:03 -07:00
wltsmrz
06288e798e Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop 2014-03-25 17:23:31 -07:00
wltsmrz
0de7d84862 Use LRU cache API to prevent multiple transaction events for the same transaction 2014-03-25 17:23:28 -07:00
Stefan Thomas
58afce517a [DOCS] Add npm badge to README. 2014-03-24 04:47:37 +01:00
Stefan Thomas
87ba2abc9a [BUG] Undo previous commit making append_byte_array too loose.
This commit introduces an alternative way of setting the canonical signature
flag, without compromising the strictness of append_byte_array input
sanitizing.
2014-03-22 03:18:11 -07:00
Stefan Thomas
716fd0b938 [CHORE] Improved Amount#parse_quality w/ demurrage support, drops->XRP, etc.
Amount#parse_quality is made currency-aware. This allows it to adjust for XRP as
the base currency, as well as for interest-bearing or demurring base currencies.
2014-03-22 02:38:03 -07:00
Stefan Thomas
893fc4c168 [CHORE] Add Amount#invert mathematical utility function. 2014-03-22 00:47:41 -07:00
Stefan Thomas
6f5cf8506f [CHORE] Better variable names in Amount#parse_quality. 2014-03-21 23:56:13 -07:00
Stefan Thomas
c808cb0a1c [CHORE] Add ability to apply demurrage at the time of product/ratio calculation. 2014-03-21 18:43:22 -07:00
Stefan Thomas
5f677a86a7 [CHORE] Update SJCL. 2014-03-20 17:38:46 -07:00
Stefan Thomas
11540f8cd9 [CHORE] Allow integer strings for server "port" setting. 2014-03-20 17:38:46 -07:00
Stefan Thomas
9d6ccdcab1 [CHORE] Enable signature canonicalization. 2014-03-20 17:38:46 -07:00
75 changed files with 12707 additions and 2055 deletions

4
.gitignore vendored
View File

@@ -42,3 +42,7 @@ test/config.js
/lib-cov
/src-cov
/coverage.html
/coverage
# Ignore IntelliJ files
.idea

View File

@@ -1,6 +1,6 @@
language: node_js
node_js:
- "0.10"
notifications:
email:
false
script: npm test --coverage
after_success:
- npm run coveralls

View File

@@ -36,12 +36,15 @@ module.exports = function(grunt) {
"src/js/sjcl/core/bn.js",
"src/js/sjcl/core/ecc.js",
"src/js/sjcl/core/srp.js",
"src/js/sjcl-custom/sjcl-ecc-pointextras.js",
"src/js/sjcl-custom/sjcl-secp256k1.js",
"src/js/sjcl-custom/sjcl-ripemd160.js",
"src/js/sjcl-custom/sjcl-extramath.js",
"src/js/sjcl-custom/sjcl-montgomery.js",
"src/js/sjcl-custom/sjcl-validecc.js",
"src/js/sjcl-custom/sjcl-ecdsa-canonical.js",
"src/js/sjcl-custom/sjcl-ecdsa-der.js",
"src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js",
"src/js/sjcl-custom/sjcl-jacobi.js"
],
dest: 'build/sjcl.js'

120
Gulpfile.js Normal file
View File

@@ -0,0 +1,120 @@
var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var webpack = require('webpack');
var jshint = require('gulp-jshint');
var map = require('map-stream');
//var header = require('gulp-header');
var pkg = require('./package.json');
var banner = '/*! <%= pkg.name %> - v<%= pkg.version %> - '
+ '<%= new Date().toISOString() %>\n'
+ '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>'
+ '* Copyright (c) <%= new Date().getFullYear() %> <%= pkg.author.name %>;'
+ ' Licensed <%= pkg.license %> */'
var sjclSrc = [
'src/js/sjcl/core/sjcl.js',
'src/js/sjcl/core/aes.js',
'src/js/sjcl/core/bitArray.js',
'src/js/sjcl/core/codecString.js',
'src/js/sjcl/core/codecHex.js',
'src/js/sjcl/core/codecBase64.js',
'src/js/sjcl/core/codecBytes.js',
'src/js/sjcl/core/sha256.js',
'src/js/sjcl/core/sha512.js',
'src/js/sjcl/core/sha1.js',
'src/js/sjcl/core/ccm.js',
// 'src/js/sjcl/core/cbc.js',
// 'src/js/sjcl/core/ocb2.js',
'src/js/sjcl/core/hmac.js',
'src/js/sjcl/core/pbkdf2.js',
'src/js/sjcl/core/random.js',
'src/js/sjcl/core/convenience.js',
'src/js/sjcl/core/bn.js',
'src/js/sjcl/core/ecc.js',
'src/js/sjcl/core/srp.js',
'src/js/sjcl-custom/sjcl-ecc-pointextras.js',
'src/js/sjcl-custom/sjcl-secp256k1.js',
'src/js/sjcl-custom/sjcl-ripemd160.js',
'src/js/sjcl-custom/sjcl-extramath.js',
'src/js/sjcl-custom/sjcl-montgomery.js',
'src/js/sjcl-custom/sjcl-validecc.js',
'src/js/sjcl-custom/sjcl-ecdsa-canonical.js',
'src/js/sjcl-custom/sjcl-ecdsa-der.js',
'src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js',
'src/js/sjcl-custom/sjcl-jacobi.js'
];
gulp.task('concat-sjcl', function() {
return gulp.src(sjclSrc)
.pipe(concat('sjcl.js'))
.pipe(gulp.dest('./build/'));
});
gulp.task('build', [ 'concat-sjcl' ], function(callback) {
webpack({
cache: true,
entry: './src/js/ripple/index.js',
output: {
library: 'ripple',
path: './build/',
filename: [ 'ripple-', '.js' ].join(pkg.version)
},
}, callback);
});
gulp.task('build-min', [ 'build' ], function(callback) {
return gulp.src([ './build/ripple-', '.js' ].join(pkg.version))
.pipe(uglify())
.pipe(rename([ 'ripple-', '-min.js' ].join(pkg.version)))
.pipe(gulp.dest('./build/'));
});
gulp.task('build-debug', [ 'concat-sjcl' ], function(callback) {
webpack({
cache: true,
entry: './src/js/ripple/index.js',
output: {
library: 'ripple',
path: './build/',
filename: [ 'ripple-', '-debug.js' ].join(pkg.version)
},
debug: true,
devtool: 'eval'
}, callback);
});
gulp.task('lint', function() {
gulp.src('src/js/ripple/*.js')
.pipe(jshint())
.pipe(map(function(file, callback) {
if (!file.jshint.success) {
console.log('\nIn', file.path);
file.jshint.results.forEach(function(err) {
if (err && err.error) {
var col1 = err.error.line + ':' + err.error.character;
var col2 = '[' + err.error.reason + ']';
var col3 = '(' + err.error.code + ')';
while (col1.length < 8) {
col1 += ' ';
}
console.log(' ' + [ col1, col2, col3 ].join(' '));
}
});
}
callback(null, file);
}));
});
gulp.task('watch', function() {
gulp.watch('src/js/ripple/*', [ 'build-debug' ]);
});
gulp.task('default', [ 'concat-sjcl', 'build', 'build-debug', 'build-min' ]);

View File

@@ -1,3 +1,34 @@
##0.7.37
+ **Deprecations**
1. Removed humanistic amount detection in `transaction.payment`. Passing `1XRP` as the payment amount no longer works.
2. `remote.setServer` uses full server URL rather than hostname. Example: `remote.setServer('wss://s`.ripple.com:443')`
3. Removed constructors for deprecated transaction types from `transaction.js`.
4. Removed `invoiceID` option from `transaction.payment`. Instead, use the `transaction.invoiceID` method.
5. Removed `transaction.transactionManager` getter.
+ Improved multi-server support. Servers are now ranked dynamically, and transactions are broadcasted to all connected servers.
+ Automatically ping connected servers. Client configuration now should contain `ping: <seconds>` to specify the ping interval.
+ Added `transaction.lastLedger` to specify `LastLedgerSequence`. Setting it this way also ensures that the sequence is not bumped on subsequent requests.
+ Added optional `remote.accountTx` binary parsing.
```js
{
binary: true,
parseBinary: false
}
```
+ Added full currency name support, e.g. `Currency.from_json('XRP').to_human({full_name:'Ripples'})` will return `XRP - Ripples`
+ Improved interest bearing currency support, e.g. `Currency.from_human('USD - US Dollar (2.5%pa)')`
+ Improve test coverage
+ Added blob vault client. The vault client facilitates interaction with ripple's namespace and blob vault or 3rd party blob vaults using ripple's blob vault software (https://github.com/ripple/ripple-blobvault). A list of the available functions can be found at [docs/VAULTCLIENT.md](docs/VAULTCLIENT.md)
##0.7.35

View File

@@ -1,12 +0,0 @@
test:
mocha --reporter spec test/*-test.js
coverage:
rm -rf src-cov
mkdir src-cov
mkdir src-cov/js
jscoverage --no-highlight src/js/ripple src-cov/js/ripple
RIPPLE_LIB_COV=1 mocha --reporter html-cov test/*-test.js > coverage.html
rm -rf src-cov
.PHONY: test

View File

@@ -1,8 +1,12 @@
#The Ripple JavaScript Library
[![Build Status](https://travis-ci.org/ripple/ripple-lib.svg?branch=develop)](https://travis-ci.org/ripple/ripple-lib) [![Coverage Status](https://coveralls.io/repos/ripple/ripple-lib/badge.png?branch=develop)](https://coveralls.io/r/ripple/ripple-lib?branch=develop)
[![NPM](https://nodei.co/npm/ripple-lib.png)](https://www.npmjs.org/package/ripple-lib)
`ripple-lib` connects to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser.
**Use ripple-lib for**
###Use ripple-lib for:
+ Connecting to a local or remote rippled in JavaScript (Node.js or browser)
+ Issuing [rippled API](https://ripple.com/wiki/JSON_Messages) requests
@@ -90,4 +94,4 @@ See [The `ripple-lib` Guides](docs/GUIDES.md) and [The `ripple-lib` API Referenc
**Generating code coverage**
ripple-lib uses `jscoverage` to generate code coverage. To generate a file `coverage.html`, run `make coverage`
ripple-lib uses `istanbul` to generate code coverage. To create a code coverage report, run `npm test --coverage`. The report will be created in `coverage/lcov-report/`.

View File

@@ -2914,8 +2914,16 @@ sjcl.ecc.point = function(curve,x,y) {
if (x === undefined) {
this.isIdentity = true;
} else {
if (x instanceof sjcl.bn) {
x = new curve.field(x);
}
if (y instanceof sjcl.bn) {
y = new curve.field(y);
}
this.x = x;
this.y = y;
this.isIdentity = false;
}
this.curve = curve;
@@ -3428,6 +3436,90 @@ sjcl.keyexchange.srp = {
};
/**
* Check that the point is valid based on the method described in
* SEC 1: Elliptic Curve Cryptography, section 3.2.2.1:
* Elliptic Curve Public Key Validation Primitive
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isValidPoint = function() {
var self = this;
var field_modulus = self.curve.field.modulus;
if (self.isIdentity) {
return false;
}
// Check that coordinatres are in bounds
// Return false if x < 1 or x > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.x)) &&
!self.x.equals(1)) ||
(self.x.greaterEquals(field_modulus.sub(1))) &&
!self.x.equals(1)) {
return false;
}
// Return false if y < 1 or y > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.y)) &&
!self.y.equals(1)) ||
(self.y.greaterEquals(field_modulus.sub(1))) &&
!self.y.equals(1)) {
return false;
}
if (!self.isOnCurve()) {
return false;
}
// TODO check to make sure point is a scalar multiple of base_point
return true;
};
/**
* Check that the point is on the curve
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isOnCurve = function() {
var self = this;
var field_order = self.curve.r;
var component_a = self.curve.a;
var component_b = self.curve.b;
var field_modulus = self.curve.field.modulus;
var left_hand_side = self.y.mul(self.y).mod(field_modulus);
var right_hand_side = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus);
return left_hand_side.equals(right_hand_side);
};
sjcl.ecc.point.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() +
')';
};
sjcl.ecc.pointJac.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() + ', ' +
this.z.toString() +
')';
};
// ----- for secp256k1 ------
// Overwrite NIST-P256 with secp256k1
@@ -3492,7 +3584,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () {
var f = e.square();
var x = f.sub(d.copy().doubleM());
var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM());
var z = this.y.mul(this.z).doubleM();
var z = this.z.mul(this.y).doubleM();
return new sjcl.ecc.pointJac(this.curve, x, y, z);
};
@@ -4012,37 +4104,63 @@ sjcl.bn.prototype.powermodMontgomery = function (e, m)
return z.revert(r);
}
sjcl.ecc.ecdsa.secretKey.prototype = {
sign: function(hash, paranoia) {
var R = this._curve.r,
l = R.bitLength(),
k = sjcl.bn.random(R.sub(1), paranoia).add(1),
r = this._curve.G.mult(k).x.mod(R),
s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) {
var R = this._curve.r,
l = R.bitLength();
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
// k_for_testing should ONLY BE SPECIFIED FOR TESTING
// specifying it will make the signature INSECURE
var k;
if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') {
k = k_for_testing;
} else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) {
k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing));
} else {
// This is the only option that should be used in production
k = sjcl.bn.random(R.sub(1), paranoia).add(1);
}
var r = this._curve.G.mult(k).x.mod(R);
var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
};
sjcl.ecc.ecdsa.publicKey.prototype = {
verify: function(hash, rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength(),
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
sInv = s.inverseMod(R),
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
hA = r.mul(sInv).mod(R),
r2 = this._curve.G.mult2(hG, hA, this._point).x;
sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength(),
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
sInv = s.inverseMod(R),
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
hA = r.mul(sInv).mod(R),
r2 = this._curve.G.mult2(hG, hA, this._point).x;
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
throw (new sjcl.exception.corrupt("signature didn't check out"));
}
return true;
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
throw (new sjcl.exception.corrupt("signature didn't check out"));
}
return true;
};
sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength();
var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l));
// For a canonical signature we want the lower of two possible values for s
// 0 < s <= n/2
if (!R.copy().halveM().greaterEquals(s)) {
s = R.sub(s);
}
return w.concat(r.toBits(l), s.toBits(l));
};
sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) {
return this.encodeDER(this.sign(hash, paranoia));
};
@@ -4078,6 +4196,313 @@ sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) {
};
/**
* This module uses the public key recovery method
* described in SEC 1: Elliptic Curve Cryptography,
* section 4.1.6, "Public Key Recovery Operation".
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* Implementation based on:
* https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js
*/
// Defined here so that this value only needs to be calculated once
var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR;
/**
* Sign the given hash such that the public key, prepending an extra byte
* so that the public key will be recoverable from the signature
*
* @param {bitArray} hash
* @param {Number} paranoia
* @returns {bitArray} Signature formatted as bitArray
*/
sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) {
var self = this;
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
// Sign hash with standard, canonicalized method
var standard_signature = self.sign(hash_bits, paranoia, k_for_testing);
var canonical_signature = self.canonicalizeSignature(standard_signature);
// Extract r and s signature components from canonical signature
var r_and_s = getRandSFromSignature(self._curve, canonical_signature);
// Rederive public key
var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get()));
// Determine recovery factor based on which possible value
// returns the correct public key
var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key);
// Prepend recovery_factor to signature and encode in DER
// The value_to_prepend should be 4 bytes total
var value_to_prepend = recovery_factor + 27;
var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature);
// Return value in bits
return final_signature_bits;
};
/**
* Recover the public key from a signature created with the
* signWithRecoverablePublicKey method in this module
*
* @static
*
* @param {bitArray} hash
* @param {bitArray} signature
* @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve
* @returns {sjcl.ecc.ecdsa.publicKey} Public key
*/
sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) {
if (!signature || signature instanceof sjcl.ecc.curve) {
throw new sjcl.exception.invalid('must supply hash and signature to recover public key');
}
if (!curve) {
curve = sjcl.ecc.curves['c256'];
}
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
var signature_bits;
if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') {
signature_bits = signature;
} else {
throw new sjcl.exception.invalid('signature. Must be a bitArray');
}
// Extract recovery_factor from first 4 bytes
var recovery_factor = signature_bits[0] - 27;
if (recovery_factor < 0 || recovery_factor > 3) {
throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' +
'that prepends the recovery factor in order to recover the public key');
}
// Separate r and s values
var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1));
var signature_r = r_and_s.r;
var signature_s = r_and_s.s;
// Recover public key using recovery_factor
var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor);
var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point);
return recovered_public_key;
};
/**
* Retrieve the r and s components of a signature
*
* @param {sjcl.ecc.curve} curve
* @param {bitArray} signature
* @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn
*/
function getRandSFromSignature(curve, signature) {
var r_length = curve.r.bitLength();
return {
r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)),
s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature)))
};
};
/**
* Determine the recovery factor by trying all four
* possibilities and figuring out which results in the
* correct public key
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {sjcl.ecc.point} original_public_key_point
* @returns {Number, 0-3} Recovery factor
*/
function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) {
var original_public_key_point_bits = original_public_key_point.toBits();
// TODO: verify that it is possible for the recovery_factor to be 2 or 3,
// we may only need 1 bit because the canonical signature might remove the
// possibility of us needing to "use the second candidate key"
for (var possible_factor = 0; possible_factor < 4; possible_factor++) {
var resulting_public_key_point;
try {
resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor);
} catch (err) {
// console.log(err, err.stack);
continue;
}
if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) {
return possible_factor;
}
}
throw new sjcl.exception.bug('unable to calculate recovery factor from signature');
};
/**
* Recover the public key from the signature.
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {Number, 0-3} recovery_factor
* @returns {sjcl.point} Public key corresponding to signature
*/
function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) {
var field_order = curve.r;
var field_modulus = curve.field.modulus;
// Reduce the recovery_factor to the two bits used
recovery_factor = recovery_factor & 3;
// The less significant bit specifies whether the y coordinate
// of the compressed point is even or not.
var compressed_point_y_coord_is_even = recovery_factor & 1;
// The more significant bit specifies whether we should use the
// first or second candidate key.
var use_second_candidate_key = recovery_factor >> 1;
// Calculate (field_order + 1) / 4
if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) {
FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4);
}
// In the paper they write "1. For j from 0 to h do the following..."
// That is not necessary here because we are given the recovery_factor
// step 1.1 Let x = r + jn
// Here "j" is either 0 or 1
var x;
if (use_second_candidate_key) {
x = signature_r.add(field_order);
} else {
x = signature_r;
}
// step 1.2 and 1.3 convert x to an elliptic curve point
// Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion
var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus);
var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus);
// If beta is even but y isn't or
// if beta is odd and y is even
// then subtract beta from the field_modulus
var y;
var beta_is_even = beta.mod(2).equals(0);
if (beta_is_even && !compressed_point_y_coord_is_even ||
!beta_is_even && compressed_point_y_coord_is_even) {
y = beta;
} else {
y = field_modulus.sub(beta);
}
// generated_point_R is the point generated from x and y
var generated_point_R = new sjcl.ecc.point(curve, x, y);
// step 1.4 check that R is valid and R x field_order !== infinity
// TODO: add check for R x field_order === infinity
if (!generated_point_R.isValidPoint()) {
throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key');
}
// step 1.5 Compute e from M
var message_e = sjcl.bn.fromBits(hash_bits);
var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order);
// step 1.6 Compute Q = r^-1 (sR - eG)
// console.log('r: ', signature_r);
var signature_r_inv = signature_r.inverseMod(field_order);
var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv);
// Validate public key point
if (!public_key_point.isValidPoint()) {
throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key');
}
// Verify that this public key matches the signature
if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) {
throw new sjcl.exception.corrupt('cannot recover public key');
}
return public_key_point;
};
/**
* Verify a signature given the raw components
* using method defined in section 4.1.5:
* "Alternative Verifying Operation"
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} e
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {sjcl.ecc.point} public_key_point
* @returns {Boolean}
*/
function verify_raw(curve, e, r, s, public_key_point) {
var field_order = curve.r;
// Return false if r is out of bounds
if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Return false if s is out of bounds
if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Check that r = (u1 + u2)G
// u1 = e x s^-1 (mod field_order)
// u2 = r x s^-1 (mod field_order)
var s_mod_inverse_field_order = s.inverseMod(field_order);
var u1 = e.mul(s_mod_inverse_field_order).mod(field_order);
var u2 = r.mul(s_mod_inverse_field_order).mod(field_order);
var point_computed = curve.G.mult2(u1, u2, public_key_point);
return r.equals(point_computed.x.mod(field_order));
};
sjcl.bn.prototype.jacobi = function (that) {
var a = this;
that = new sjcl.bn(that);

168
docs/VAULTCLIENT.md Normal file
View File

@@ -0,0 +1,168 @@
ripple-vault-client
===================
A javascript / http client to interact with Ripple Vault servers.
The purpose of this tool is to enable applications in any javascript
environment to login with the ripple vault and access the decrypted
data stored using credentials originally obtained at ripple.com
## Vault Client Usage
vaultClient = new ripple.VaultClient(domain);
vaultClient.getAuthInfo(username, callback);
vaultClient.getRippleName(address, url, callback);
vaultClient.exists(username, callback);
vaultClient.login(username, password, callback);
vaultClient.relogin(id, cryptKey, callback);
vaultClient.unlock(username, password, encryptSecret, callback);
vaultClient.loginAndUnlock(username, password, callback);
vaultClient.register(options, callback);
vaultClient.deleteBlob(options, callback);
vaultClient.recoverBlob(options, callback);
vaultClient.rename(options, callback);
vaultClient.changePassword(options, callback);
vaultClient.verify(username, token, callback);
vaultClient.resendEmail(options, callback);
vaultClient.updateProfile(options, fn);
# Blob Methods
blob.encrypt();
blob.decrypt(encryptedBlob);
blob.encryptSecret(encryptionKey);
blob.decryptSecret(encryptionKey, secret);
blob.set(pointer, value, callback);
blob.unset(pointer, callback);
blob.extend(pointer, value, callback);
blob.unshift(pointer, value, callback);
blob.filter(pointer, field, value, subcommands, callback);
## Identity Vault
The identity vault stores identity information inside the encrypted
blob vault. The identity fields can be additionally encrypted with the
unlock key, that encrypts the secret, for added security. Methods are
accessed from the 'identity' property of the blob object.
# Identity fields
+ name
+ entityType (individual, corporation, organization)
+ email
+ phone
+ address
+ contact
+ line1
+ line2
+ city
+ postalCode
+ region - state/province/region
+ country
+ nationalID
+ number
+ type (ssn, taxID, passport, driversLicense, other)
+ country - issuing country
+ birthday
+ birthplace
# Identity Methods
blob.identity.set(pointer, key, value, callback);
blob.identity.unset(pointer, key, callback);
blob.identity.get(pointer, key);
blob.identity.getAll(key);
blob.identity.getFullAddress(key); //get text string of full address
## Spec Tests
Run `npm test` to test the high-level behavior specs
Ripple Txt
✓ should get the content of a ripple.txt file from a given domain
✓ should get currencies from a ripple.txt file for a given domain
✓ should get the domain from a given url
AuthInfo
✓ should get auth info
VaultClient
#initialization
✓ should be initialized with a domain
✓ should default to ripple.com without a domain
#exists
✓ should determine if a username exists on the domain
#login
✓ with username and password should retrive the blob, crypt key, and id
#relogin
✓ should retrieve the decrypted blob with blob vault url, id, and crypt key
#unlock
✓ should access the wallet secret using encryption secret, username and password
#loginAndUnlock
✓ should get the decrypted blob and decrypted secret given name and password
#register
✓ should create a new blob
#deleteBlob
✓ should remove an existing blob
#updateProfile
✓ should update profile parameters associated with a blob
Blob
#set
#extend
#unset
#unshift
#filter
#consolidate
#rename
✓ should change the username of a blob
#changePassword
✓ should change the password and keys of a blob
#recoverBlob
✓ should recover the blob given a username and secret
#verifyEmail
✓ should verify an email given a username and token
#resendVerifcationEmail
✓ should resend a verification given options
identity
#identity_set
#identity_get
#identity_getAll
#identity_getFullAddress
#identity_unset

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "0.7.35",
"version": "0.7.37",
"description": "Ripple JavaScript client library",
"files": [
"src/js/*",
@@ -8,34 +8,37 @@
"build/*",
"test/*",
"Makefile",
"Gruntfile.js"
"Gulpfile.js"
],
"main": "src/js/ripple",
"directories": {
"test": "test"
},
"dependencies": {
"async": "~0.2.10",
"async": "~0.8.0",
"ws": "~0.4.31",
"extend": "~1.2.1",
"jshint-loader": "~0.5.0",
"lru-cache": "~2.5.0"
"lru-cache": "~2.5.0",
"superagent": "^0.18.0",
"gulp-uglify": "~0.3.0",
"gulp-rename": "~1.2.0"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-cli": "~0.1.9",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-watch": "~0.4.4",
"grunt-webpack": "~0.10.5",
"grunt-dox": "~0.5.0",
"mocha": "~1.14.0",
"sinon-chai": "~2.4.0",
"sinon": "~1.7.3"
"gulp": "~3.6.2",
"gulp-concat": "~2.2.0",
"gulp-jshint": "~1.5.5",
"webpack": "~1.1.11",
"map-stream": "~0.1.0",
"istanbul": "~0.2.10",
"coveralls": "~2.10.0",
"nock": "^0.34.1"
},
"scripts": {
"pretest": "node_modules/.bin/grunt",
"test": "mocha test/*-test.js",
"build": "grunt"
"build": "node_modules/.bin/gulp",
"pretest": "node_modules/.bin/gulp concat-sjcl",
"test": "./node_modules/.bin/istanbul test -x build/sjcl.js -x src/js/jsbn/* ./node_modules/.bin/_mocha -- --reporter spec test/*-test.js",
"coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls"
},
"repository": {
"type": "git",

43
scripts/verify_ledger_json.js Normal file → Executable file
View File

@@ -1,14 +1,49 @@
var fs = require('fs');
var Ledger = require('../src/js/ripple/ledger').Ledger;
if (process.argc < 1) {
function parse_options(from, flags) {
var argv = from.slice(),
opts = {argv:argv};
flags.forEach(function(f) {
// Do we have the flag?
var flag_index = argv.indexOf('--' + f);
// normalize the name of the flag
f = f.replace('-', '_');
// opts has Boolean value for normalized flag key
opts[f] = !!~flag_index;
if (opts[f]) {
// remove the flag from the argv
argv.splice(flag_index, 1);
}
});
return opts;
}
var opts = parse_options(process.argv.slice(2), // remove `node` and `this.js`
['sanity-test']);
if (opts.argv.length < 1) {
console.error("Usage: scripts/verify_ledger_json path/to/ledger.json");
console.error(" optional: --sanity-test (json>binary>json>binary)");
process.exit(1);
}
var json = fs.readFileSync(process.argv[2], 'utf-8');
var json = fs.readFileSync(opts.argv[0], 'utf-8');
var ledger = Ledger.from_json(JSON.parse(json));
console.log("Calculated transaction hash: "+ledger.calc_tx_hash().to_hex())
console.log("Transaction hash in header: "+ledger.ledger_json.transaction_hash);
// This will serialize each accountState object to binary and then back to json
// before finally serializing for hashing. This is mostly to expose any issues
// with ripple-libs binary <--> json codecs.
if (opts.sanity_test) {
console.log("All accountState nodes will be processed from " +
"json->binary->json->binary. This may take some time " +
"with large ledgers.");
}
console.log("Transaction hash in header: " + ledger.ledger_json.transaction_hash);
console.log("Calculated transaction hash: " + ledger.calc_tx_hash().to_hex());
console.log("Account state hash in header: " + ledger.ledger_json.account_hash);
console.log("Calculated account state hash: " + ledger.calc_account_hash(
{sanity_test:opts.sanity_test})
.to_hex());

View File

@@ -9,14 +9,16 @@
// balance_proposed
//
// var network = require("./network.js");
var EventEmitter = require('events').EventEmitter;
// var network = require('./network.js');
var async = require('async');
var util = require('util');
var extend = require('extend');
var EventEmitter = require('events').EventEmitter;
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var TransactionManager = require('./transactionmanager').TransactionManager;
var sjcl = require('./utils').sjcl;
var Base = require('./base').Base;
/**
* @constructor Account
@@ -29,10 +31,10 @@ function Account(remote, account) {
var self = this;
this._remote = remote;
this._account = UInt160.from_json(account);
this._remote = remote;
this._account = UInt160.from_json(account);
this._account_id = this._account.to_json();
this._subs = 0;
this._subs = 0;
// Ledger entry object
// Important: This must never be overwritten, only extend()-ed
@@ -73,7 +75,9 @@ function Account(remote, account) {
this._remote.on('prepare_subscribe', attachAccount);
function handleTransaction(transaction) {
if (!transaction.mmeta) return;
if (!transaction.mmeta) {
return;
}
var changed = false;
@@ -135,7 +139,7 @@ Account.prototype.getInfo = function(callback) {
* Retrieve the current AccountRoot entry.
*
* To keep up-to-date with changes to the AccountRoot entry, subscribe to the
* "entry" event.
* 'entry' event.
*
* @param {Function} callback
*/
@@ -162,11 +166,14 @@ Account.prototype.entry = function(callback) {
Account.prototype.getNextSequence = function(callback) {
var callback = typeof callback === 'function' ? callback : function(){};
function isNotFound(err) {
return err && typeof err === 'object'
&& typeof err.remote === 'object'
&& err.remote.error === 'actNotFound';
};
function accountInfo(err, info) {
if (err &&
"object" === typeof err &&
"object" === typeof err.remote &&
err.remote.error === "actNotFound") {
if (isNotFound(err)) {
// New accounts will start out as sequence zero
callback(null, 0);
} else if (err) {
@@ -185,7 +192,7 @@ Account.prototype.getNextSequence = function(callback) {
* Retrieve this account's Ripple trust lines.
*
* To keep up-to-date with changes to the AccountRoot entry, subscribe to the
* "lines" event. (Not yet implemented.)
* 'lines' event. (Not yet implemented.)
*
* @param {function(err, lines)} callback Called with the result
*/
@@ -218,23 +225,27 @@ Account.prototype.lines = function(callback) {
* @returns {Account}
*/
Account.prototype.line = function(currency,address,callback) {
Account.prototype.line = function(currency, address, callback) {
var self = this;
var found;
var callback = typeof callback === 'function' ? callback : function(){};
self.lines(function(err, data) {
if (err) {
callback(err);
} else {
var line = data.lines.filter(function(line) {
if (line.account === address && line.currency === currency) {
return line;
}
})[0];
callback(null, line);
return callback(err);
}
var line;
top:
for (var i=0; i<data.lines.length; i++) {
var l = data.lines[i];
if (l.account === address && l.currency === currency) {
line = l;
break top;
}
}
callback(null, line);
});
return this;
@@ -254,17 +265,21 @@ Account.prototype.notifyTx = function(transaction) {
// Only trigger the event if the account object is actually
// subscribed - this prevents some weird phantom events from
// occurring.
if (this._subs) {
this.emit('transaction', transaction);
var account = transaction.transaction.Account;
if (!account) return;
var isThisAccount = account === this._account_id;
this.emit(isThisAccount ? 'transaction-outbound' : 'transaction-inbound', transaction);
if (!this._subs) {
return;
}
this.emit('transaction', transaction);
var account = transaction.transaction.Account;
if (!account) {
return;
}
var isThisAccount = (account === this._account_id);
this.emit(isThisAccount ? 'transaction-outbound' : 'transaction-inbound', transaction);
};
/**
@@ -278,6 +293,104 @@ Account.prototype.submit = function(transaction) {
this._transactionManager.submit(transaction);
};
/**
* Check whether the given public key is valid for this account
*
* @param {Hex-encoded String|RippleAddress} public_key
* @param {Function} callback
*
* @callback
* @param {Error} err
* @param {Boolean} true if the public key is valid and active, false otherwise
*/
Account.prototype.publicKeyIsActive = function(public_key, callback) {
var self = this;
var public_key_as_uint160;
try {
public_key_as_uint160 = Account._publicKeyToAddress(public_key);
} catch (err) {
return callback(err);
}
function getAccountInfo(async_callback) {
self.getInfo(function(err, account_info_res){
// If the remote responds with an Account Not Found error then the account
// is unfunded and thus we can assume that the master key is active
if (err && err.remote && err.remote.error === 'actNotFound') {
async_callback(null, null);
} else {
async_callback(err, account_info_res);
}
});
};
function publicKeyIsValid(account_info_res, async_callback) {
// Catch the case of unfunded accounts
if (!account_info_res) {
if (public_key_as_uint160 === self._account_id) {
async_callback(null, true);
} else {
async_callback(null, false);
}
return;
}
var account_info = account_info_res.account_data;
// Respond with true if the RegularKey is set and matches the given public key or
// if the public key matches the account address and the lsfDisableMaster is not set
if (account_info.RegularKey &&
account_info.RegularKey === public_key_as_uint160) {
async_callback(null, true);
} else if (account_info.Account === public_key_as_uint160 &&
((account_info.Flags & 0x00100000) === 0)) {
async_callback(null, true);
} else {
async_callback(null, false);
}
};
var steps = [
getAccountInfo,
publicKeyIsValid
];
async.waterfall(steps, callback);
};
/**
* Convert a hex-encoded public key to a Ripple Address
*
* @static
*
* @param {Hex-encoded string|RippleAddress} public_key
* @returns {RippleAddress}
*/
Account._publicKeyToAddress = function(public_key) {
// Based on functions in /src/js/ripple/keypair.js
function hexToUInt160(public_key) {
var bits = sjcl.codec.hex.toBits(public_key);
var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
var address = UInt160.from_bits(hash);
address.set_version(Base.VER_ACCOUNT_ID);
return address.to_json();
};
if (UInt160.is_valid(public_key)) {
return public_key;
} else if (/^[0-9a-fA-F]+$/.test(public_key)) {
return hexToUInt160(public_key);
} else {
throw new Error('Public key is invalid. Must be a UInt160 or a hex string');
}
};
exports.Account = Account;
// vim:sw=2:sts=2:ts=8:et

View File

@@ -54,52 +54,52 @@ function Amount() {
};
// Given '100/USD/mtgox' return the a string with mtgox remapped.
Amount.text_full_rewrite = function (j) {
Amount.text_full_rewrite = function(j) {
return Amount.from_json(j).to_text_full();
};
// Given '100/USD/mtgox' return the json.
Amount.json_rewrite = function (j) {
Amount.json_rewrite = function(j) {
return Amount.from_json(j).to_json();
};
Amount.from_number = function (n) {
Amount.from_number = function(n) {
return (new Amount()).parse_number(n);
};
Amount.from_json = function (j) {
Amount.from_json = function(j) {
return (new Amount()).parse_json(j);
};
Amount.from_quality = function (q, c, i) {
return (new Amount()).parse_quality(q, c, i);
Amount.from_quality = function(quality, currency, issuer, opts) {
return (new Amount()).parse_quality(quality, currency, issuer, opts);
};
Amount.from_human = function (j, opts) {
Amount.from_human = function(j, opts) {
return (new Amount()).parse_human(j, opts);
};
Amount.is_valid = function (j) {
Amount.is_valid = function(j) {
return Amount.from_json(j).is_valid();
};
Amount.is_valid_full = function (j) {
Amount.is_valid_full = function(j) {
return Amount.from_json(j).is_valid_full();
};
Amount.NaN = function () {
Amount.NaN = function() {
var result = new Amount();
result._value = NaN;
return result;
};
// Returns a new value which is the absolute value of this.
Amount.prototype.abs = function () {
Amount.prototype.abs = function() {
return this.clone(this.is_negative());
};
// Result in terms of this' currency and issuer.
Amount.prototype.add = function (v) {
Amount.prototype.add = function(v) {
var result;
v = Amount.from_json(v);
@@ -140,18 +140,18 @@ Amount.prototype.add = function (v) {
o2 += 1;
}
result = new Amount();
result._is_native = false;
result._offset = o1;
result._value = v1.add(v2);
result = new Amount();
result._is_native = false;
result._offset = o1;
result._value = v1.add(v2);
result._is_negative = result._value.compareTo(BigInteger.ZERO) < 0;
if (result._is_negative) {
result._value = result._value.negate();
result._value = result._value.negate();
}
result._currency = this._currency;
result._issuer = this._issuer;
result._currency = this._currency;
result._issuer = this._issuer;
result.canonicalize();
}
@@ -159,7 +159,30 @@ Amount.prototype.add = function (v) {
return result;
};
Amount.prototype.canonicalize = function () {
/**
* Turn this amount into its inverse.
*
* @private
*/
Amount.prototype._invert = function() {
this._value = consts.bi_1e32.divide(this._value);
this._offset = -32 - this._offset;
this.canonicalize();
return this;
};
/**
* Return the inverse of this amount.
*
* @return {Amount} New Amount object with same currency and issuer, but the
* inverse of the value.
*/
Amount.prototype.invert = function() {
return this.copy()._invert();
};
Amount.prototype.canonicalize = function() {
if (!(this._value instanceof BigInteger)) {
// NaN.
// nothing
@@ -203,11 +226,11 @@ Amount.prototype.canonicalize = function () {
return this;
};
Amount.prototype.clone = function (negate) {
Amount.prototype.clone = function(negate) {
return this.copyTo(new Amount(), negate);
};
Amount.prototype.compareTo = function (v) {
Amount.prototype.compareTo = function(v) {
var result;
if (!this.is_comparable(v)) {
@@ -239,7 +262,7 @@ Amount.prototype.compareTo = function (v) {
// Make d a copy of this. Returns d.
// Modification of objects internally refered to is not allowed.
Amount.prototype.copyTo = function (d, negate) {
Amount.prototype.copyTo = function(d, negate) {
if (typeof this._value === 'object') {
this._value.copyTo(d._value);
} else {
@@ -263,11 +286,11 @@ Amount.prototype.copyTo = function (d, negate) {
return d;
};
Amount.prototype.currency = function () {
Amount.prototype.currency = function() {
return this._currency;
};
Amount.prototype.equals = function (d, ignore_issuer) {
Amount.prototype.equals = function(d, ignore_issuer) {
if (typeof d === 'string') {
return this.equals(Amount.from_json(d));
}
@@ -278,17 +301,17 @@ Amount.prototype.equals = function (d, ignore_issuer) {
|| (this._is_native !== d._is_native)
|| (!this._value.equals(d._value) || this._offset !== d._offset)
|| (this._is_negative !== d._is_negative)
|| (!this._is_native && (!this._currency.equals(d._currency) || !ignore_issuer && !this._issuer.equals(d._issuer))))
|| (!this._is_native && (!this._currency.equals(d._currency) || !ignore_issuer && !this._issuer.equals(d._issuer))));
return result;
};
// Result in terms of this' currency and issuer.
Amount.prototype.divide = function (d) {
Amount.prototype.divide = function(d) {
var result;
if (d.is_zero()) {
throw 'divide by zero';
throw new Error('divide by zero');
}
if (this.is_zero()) {
@@ -349,9 +372,14 @@ Amount.prototype.divide = function (d) {
*
* @this {Amount} The numerator (top half) of the fraction.
* @param {Amount} denominator The denominator (bottom half) of the fraction.
* @param opts Options for the calculation.
* @param opts.reference_date {Date|Number} Date based on which demurrage/interest
* should be applied. Can be given as JavaScript Date or int for Ripple epoch.
* @return {Amount} The resulting ratio. Unit will be the same as numerator.
*/
Amount.prototype.ratio_human = function (denominator) {
Amount.prototype.ratio_human = function(denominator, opts) {
opts = opts || {};
if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) {
// Special handling of integer arguments
denominator = Amount.from_json('' + denominator + '.0');
@@ -367,6 +395,14 @@ Amount.prototype.ratio_human = function (denominator) {
return Amount.NaN();
}
// Apply interest/demurrage
//
// We only need to apply it to the second factor, because the currency unit of
// the first factor will carry over into the result.
if (opts.reference_date) {
denominator = denominator.applyInterest(opts.reference_date);
}
// Special case: The denominator is a native (XRP) amount.
//
// In that case, it's going to be expressed as base units (1 XRP =
@@ -402,9 +438,14 @@ Amount.prototype.ratio_human = function (denominator) {
*
* @this {Amount} The first factor of the product.
* @param {Amount} factor The second factor of the product.
* @param opts Options for the calculation.
* @param opts.reference_date {Date|Number} Date based on which demurrage/interest
* should be applied. Can be given as JavaScript Date or int for Ripple epoch.
* @return {Amount} The product. Unit will be the same as the first factor.
*/
Amount.prototype.product_human = function (factor) {
Amount.prototype.product_human = function(factor, opts) {
opts = opts || {};
if (typeof factor === 'number' && parseInt(factor, 10) === factor) {
// Special handling of integer arguments
factor = Amount.from_json(String(factor) + '.0');
@@ -417,6 +458,14 @@ Amount.prototype.product_human = function (factor) {
return Amount.NaN();
}
// Apply interest/demurrage
//
// We only need to apply it to the second factor, because the currency unit of
// the first factor will carry over into the result.
if (opts.reference_date) {
factor = factor.applyInterest(opts.reference_date);
}
var product = this.multiply(factor);
// Special case: The second factor is a native (XRP) amount expressed as base
@@ -429,49 +478,49 @@ Amount.prototype.product_human = function (factor) {
}
return product;
}
};
// True if Amounts are valid and both native or non-native.
Amount.prototype.is_comparable = function (v) {
Amount.prototype.is_comparable = function(v) {
return this._value instanceof BigInteger
&& v._value instanceof BigInteger
&& this._is_native === v._is_native;
};
Amount.prototype.is_native = function () {
Amount.prototype.is_native = function() {
return this._is_native;
};
Amount.prototype.is_negative = function () {
Amount.prototype.is_negative = function() {
return this._value instanceof BigInteger
? this._is_negative
: false; // NaN is not negative
};
Amount.prototype.is_positive = function () {
Amount.prototype.is_positive = function() {
return !this.is_zero() && !this.is_negative();
};
// Only checks the value. Not the currency and issuer.
Amount.prototype.is_valid = function () {
Amount.prototype.is_valid = function() {
return this._value instanceof BigInteger;
};
Amount.prototype.is_valid_full = function () {
Amount.prototype.is_valid_full = function() {
return this.is_valid() && this._currency.is_valid() && this._issuer.is_valid();
};
Amount.prototype.is_zero = function () {
Amount.prototype.is_zero = function() {
return this._value instanceof BigInteger ? this._value.equals(BigInteger.ZERO) : false;
};
Amount.prototype.issuer = function () {
Amount.prototype.issuer = function() {
return this._issuer;
};
// Result in terms of this' currency and issuer.
// XXX Diverges from cpp.
Amount.prototype.multiply = function (v) {
Amount.prototype.multiply = function(v) {
var result;
if (this.is_zero()) {
@@ -514,7 +563,7 @@ Amount.prototype.multiply = function (v) {
};
// Return a new value.
Amount.prototype.negate = function () {
Amount.prototype.negate = function() {
return this.clone('NEGATE');
};
@@ -524,7 +573,7 @@ Amount.prototype.negate = function () {
* Creates a new Amount object as a copy of the current one (including the same
* unit (currency & issuer), inverts it (1/x) and returns the result.
*/
Amount.prototype.invert = function () {
Amount.prototype.invert = function() {
var one = this.clone();
one._value = BigInteger.ONE;
one._offset = 0;
@@ -544,10 +593,25 @@ Amount.prototype.invert = function () {
* 25.2 XRP => 25200000/XRP
* USD 100.40 => 100.4/USD/?
* 100 => 100000000/XRP
*
*
* The regular expression below matches above cases, broken down for better understanding:
*
* ^\s* // start with any amount of whitespace
* ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3 digit numeric currency-code. See ISO 4217
* \s* // any amount of whitespace
* (-)? // optional dash
* (\d+) // 1 or more digits
* (?:\.(\d*))? // optional . character with any amount of digits
* \s* // any amount of whitespace
* ([a-f0-9]{40}|[a-z0-9]{3})? // optional 40 character hex string OR 3 letters
* \s* // any amount of whitespace
* $ // end of string
*
*/
Amount.human_RE = /^\s*([a-z]{3})?\s*(-)?(\d+)(?:\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i;
Amount.human_RE = /^\s*([a-z]{3}|[0-9]{3})?\s*(-)?(\d+)(?:\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i;
Amount.prototype.parse_human = function (j, opts) {
Amount.prototype.parse_human = function(j, opts) {
opts = opts || {};
var m = String(j).match(Amount.human_RE);
@@ -597,7 +661,7 @@ Amount.prototype.parse_human = function (j, opts) {
// The correct way to solve this is probably to switch to a proper
// BigDecimal for our internal representation and then use that across
// the board instead of instantiating these dummy Amount objects.
var interestTempAmount = Amount.from_json(""+interest+"/1/1");
var interestTempAmount = Amount.from_json(''+interest+'/1/1');
if (interestTempAmount.is_valid()) {
var ref = this.divide(interestTempAmount);
@@ -612,31 +676,95 @@ Amount.prototype.parse_human = function (j, opts) {
return this;
};
Amount.prototype.parse_issuer = function (issuer) {
Amount.prototype.parse_issuer = function(issuer) {
this._issuer = UInt160.from_json(issuer);
return this;
};
// --> h: 8 hex bytes quality or 32 hex bytes directory index.
Amount.prototype.parse_quality = function (q, c, i) {
/**
* Decode a price from a BookDirectory index.
*
* BookDirectory ledger entries each encode the offer price in their index. This
* method can decode that information and populate an Amount object with it.
*
* It is possible not to provide a currency or issuer, but be aware that Amount
* objects behave differently based on the currency, so you may get incorrect
* results.
*
* Prices involving demurraging currencies are tricky, since they depend on the
* base and counter currencies.
*
* @param quality {String} 8 hex bytes quality or 32 hex bytes BookDirectory
* index.
* @param counterCurrency {Currency|String} Currency of the resulting Amount
* object.
* @param counterIssuer {Issuer|String} Issuer of the resulting Amount object.
* @param opts Additional options
* @param opts.inverse {Boolean} If true, return the inverse of the price
* encoded in the quality.
* @param opts.base_currency {Currency|String} The other currency. This plays a
* role with interest-bearing or demurrage currencies. In that case the
* demurrage has to be applied when the quality is decoded, otherwise the
* price will be false.
* @param opts.reference_date {Date|Number} Date based on which demurrage/interest
* should be applied. Can be given as JavaScript Date or int for Ripple epoch.
* @param opts.xrp_as_drops {Boolean} Whether XRP amount should be treated as
* drops. When the base currency is XRP, the quality is calculated in drops.
* For human use however, we want to think of 1000000 drops as 1 XRP and
* prices as per-XRP instead of per-drop.
*/
Amount.prototype.parse_quality = function(quality, counterCurrency, counterIssuer, opts)
{
opts = opts || {};
var baseCurrency = Currency.from_json(opts.base_currency);
this._is_negative = false;
this._value = new BigInteger(q.substring(q.length-14), 16);
this._offset = parseInt(q.substring(q.length-16, q.length-14), 16)-100;
this._currency = Currency.from_json(c);
this._issuer = UInt160.from_json(i);
this._value = new BigInteger(quality.substring(quality.length-14), 16);
this._offset = parseInt(quality.substring(quality.length-16, quality.length-14), 16)-100;
this._currency = Currency.from_json(counterCurrency);
this._issuer = UInt160.from_json(counterIssuer);
this._is_native = this._currency.is_native();
// Correct offset if xrp_as_drops option is not set and base currency is XRP
if (!opts.xrp_as_drops &&
baseCurrency.is_valid() &&
baseCurrency.is_native()) {
if (opts.inverse) {
this._offset -= 6;
} else {
this._offset += 6;
}
}
if (opts.inverse) {
this._invert();
}
this.canonicalize();
return this;
}
if (opts.reference_date && baseCurrency.is_valid() && baseCurrency.has_interest()) {
var interest = baseCurrency.get_interest_at(opts.reference_date);
Amount.prototype.parse_number = function (n) {
// XXX If we had better math utilities, we wouldn't need this hack.
var interestTempAmount = Amount.from_json(''+interest+'/1/1');
if (interestTempAmount.is_valid()) {
var v = this.divide(interestTempAmount);
this._value = v._value;
this._offset = v._offset;
}
}
return this;
};
Amount.prototype.parse_number = function(n) {
this._is_native = false;
this._currency = Currency.from_json(1);
this._issuer = UInt160.from_json(1);
this._is_negative = n < 0 ? 1 : 0;
this._is_negative = n < 0 ? true : false;
this._value = new BigInteger(String(this._is_negative ? -n : n));
this._offset = 0;
@@ -646,7 +774,7 @@ Amount.prototype.parse_number = function (n) {
};
// <-> j
Amount.prototype.parse_json = function (j) {
Amount.prototype.parse_json = function(j) {
switch (typeof j) {
case 'string':
// .../.../... notation is not a wire format. But allowed for easier testing.
@@ -672,7 +800,10 @@ Amount.prototype.parse_json = function (j) {
break;
case 'object':
if (j === null) break;
if (j === null) {
break;
}
if (j instanceof Amount) {
j.copyTo(this);
} else if (j.hasOwnProperty('value')) {
@@ -698,7 +829,7 @@ Amount.prototype.parse_json = function (j) {
// - integer = raw units
// - float = with precision 6
// XXX Improvements: disallow leading zeros.
Amount.prototype.parse_native = function (j) {
Amount.prototype.parse_native = function(j) {
var m;
if (typeof j === 'string') {
@@ -733,7 +864,7 @@ Amount.prototype.parse_native = function (j) {
// Parse a non-native value for the json wire format.
// Requires _currency to be set!
Amount.prototype.parse_value = function (j) {
Amount.prototype.parse_value = function(j) {
this._is_native = false;
switch (typeof j) {
@@ -787,14 +918,14 @@ Amount.prototype.parse_value = function (j) {
return this;
};
Amount.prototype.set_currency = function (c) {
Amount.prototype.set_currency = function(c) {
this._currency = Currency.from_json(c);
this._is_native = this._currency.is_native();
return this;
};
Amount.prototype.set_issuer = function (issuer) {
Amount.prototype.set_issuer = function(issuer) {
if (issuer instanceof UInt160) {
this._issuer = issuer;
} else {
@@ -805,18 +936,18 @@ Amount.prototype.set_issuer = function (issuer) {
};
// Result in terms of this' currency and issuer.
Amount.prototype.subtract = function (v) {
Amount.prototype.subtract = function(v) {
// Correctness over speed, less code has less bugs, reuse add code.
return this.add(Amount.from_json(v).negate());
};
Amount.prototype.to_number = function (allow_nan) {
Amount.prototype.to_number = function(allow_nan) {
var s = this.to_text(allow_nan);
return typeof s === 'string' ? Number(s) : s;
};
// Convert only value to JSON wire format.
Amount.prototype.to_text = function (allow_nan) {
Amount.prototype.to_text = function(allow_nan) {
var result = NaN;
if (this._is_native) {
@@ -850,6 +981,39 @@ Amount.prototype.to_text = function (allow_nan) {
return result;
};
/**
* Calculate present value based on currency and a reference date.
*
* This only affects demurraging and interest-bearing currencies.
*
* User should not store amount objects after the interest is applied. This is
* intended by display functions such as toHuman().
*
* @param referenceDate {Date|Number} Date based on which demurrage/interest
* should be applied. Can be given as JavaScript Date or int for Ripple epoch.
* @return {Amount} The amount with interest applied.
*/
Amount.prototype.applyInterest = function(referenceDate) {
if (this._currency.has_interest()) {
var interest = this._currency.get_interest_at(referenceDate);
// XXX Because the Amount parsing routines don't support some of the things
// that JavaScript can output when casting a float to a string, the
// following call sometimes does not produce a valid Amount.
//
// The correct way to solve this is probably to switch to a proper
// BigDecimal for our internal representation and then use that across
// the board instead of instantiating these dummy Amount objects.
var interestTempAmount = Amount.from_json(String(interest) + '/1/1');
if (interestTempAmount.is_valid()) {
return this.multiply(interestTempAmount);
}
} else {
return this;
}
};
/**
* Format only value in a human-readable format.
*
@@ -872,34 +1036,27 @@ Amount.prototype.to_text = function (allow_nan) {
* @param opts.reference_date {Date|Number} Date based on which demurrage/interest
* should be applied. Can be given as JavaScript Date or int for Ripple epoch.
*/
Amount.prototype.to_human = function (opts) {
Amount.prototype.to_human = function(opts) {
opts = opts || {};
if (!this.is_valid()) return '';
if (!this.is_valid()) {
return '';
}
// Default options
if (typeof opts.signed === 'undefined') opts.signed = true;
if (typeof opts.group_sep === 'undefined') opts.group_sep = true;
if (typeof opts.signed === 'undefined') {
opts.signed = true;
}
if (typeof opts.group_sep === 'undefined') {
opts.group_sep = true;
}
opts.group_width = opts.group_width || 3;
// Apply demurrage/interest
var ref = this;
if (opts.reference_date && this._currency.has_interest()) {
var interest = this._currency.get_interest_at(opts.reference_date);
// XXX Because the Amount parsing routines don't support some of the things
// that JavaScript can output when casting a float to a string, the
// following call sometimes does not produce a valid Amount.
//
// The correct way to solve this is probably to switch to a proper
// BigDecimal for our internal representation and then use that across
// the board instead of instantiating these dummy Amount objects.
var interestTempAmount = Amount.from_json(""+interest+"/1/1");
if (interestTempAmount.is_valid()) {
ref = this.multiply(interestTempAmount);
}
if (opts.reference_date) {
ref = this.applyInterest(opts.reference_date);
}
var order = ref._is_native ? consts.xns_precision : -ref._offset;
@@ -912,7 +1069,7 @@ Amount.prototype.to_human = function (opts) {
fraction_part = '0' + fraction_part;
}
int_part = int_part.replace(/^0*/, '');
int_part = int_part.replace(/^0*/, '');
fraction_part = fraction_part.replace(/0*$/, '');
if (fraction_part.length || !opts.skip_empty_fraction) {
@@ -947,7 +1104,9 @@ Amount.prototype.to_human = function (opts) {
rounding = Math.min(rounding, fraction_part.length);
// Now we cut `rounding` digits off the right.
if (rounding > 0) fraction_part = fraction_part.slice(0, -rounding);
if (rounding > 0) {
fraction_part = fraction_part.slice(0, -rounding);
}
}
// Enforce the minimum number of decimal digits (min_precision)
@@ -979,7 +1138,7 @@ Amount.prototype.to_human = function (opts) {
return formatted;
};
Amount.prototype.to_human_full = function (opts) {
Amount.prototype.to_human_full = function(opts) {
opts = opts || {};
var a = this.to_human(opts);
var c = this._currency.to_human();
@@ -988,7 +1147,7 @@ Amount.prototype.to_human_full = function (opts) {
return o;
};
Amount.prototype.to_json = function () {
Amount.prototype.to_json = function() {
var result;
if (this._is_native) {
@@ -1007,7 +1166,7 @@ Amount.prototype.to_json = function () {
return result;
};
Amount.prototype.to_text_full = function (opts) {
Amount.prototype.to_text_full = function(opts) {
return this._value instanceof BigInteger
? this._is_native
? this.to_human() + '/XRP'
@@ -1016,33 +1175,35 @@ Amount.prototype.to_text_full = function (opts) {
};
// For debugging.
Amount.prototype.not_equals_why = function (d, ignore_issuer) {
Amount.prototype.not_equals_why = function(d, ignore_issuer) {
if (typeof d === 'string') {
return this.not_equals_why(Amount.from_json(d));
}
if (!(d instanceof Amount)) {
return 'Not an Amount';
}
var result = false;
if (typeof d === 'string') {
result = this.not_equals_why(Amount.from_json(d));
} else if (d instanceof Amount) {
if (!this.is_valid() || !d.is_valid()) {
result = 'Invalid amount.';
} else if (this._is_native !== d._is_native) {
result = 'Native mismatch.';
} else {
var type = this._is_native ? 'XRP' : 'Non-XRP';
if (!this.is_valid() || !d.is_valid()) {
result = 'Invalid amount.';
} else if (this._is_native !== d._is_native) {
result = 'Native mismatch.';
} else {
var type = this._is_native ? 'XRP' : 'Non-XRP';
if (!this._value.equals(d._value) || this._offset !== d._offset) {
result = type + ' value differs.';
} else if (this._is_negative !== d._is_negative) {
result = type + ' sign differs.';
} else if (!this._is_native) {
if (!this._currency.equals(d._currency)) {
result = 'Non-XRP currency differs.';
} else if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
result = 'Non-XRP issuer differs: ' + d._issuer.to_json() + '/' + this._issuer.to_json();
}
if (!this._value.equals(d._value) || this._offset !== d._offset) {
result = type + ' value differs.';
} else if (this._is_negative !== d._is_negative) {
result = type + ' sign differs.';
} else if (!this._is_native) {
if (!this._currency.equals(d._currency)) {
result = 'Non-XRP currency differs.';
} else if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
result = 'Non-XRP issuer differs: ' + d._issuer.to_json() + '/' + this._issuer.to_json();
}
}
} else {
result = 'Wrong constructor.';
}
return result;

59
src/js/ripple/authinfo.js Normal file
View File

@@ -0,0 +1,59 @@
var async = require('async');
var superagent = require('superagent');
var RippleTxt = require('./rippletxt').RippleTxt;
var AuthInfo = { };
AuthInfo._getRippleTxt = function(domain, callback) {
RippleTxt.get(domain, callback);
};
AuthInfo._getUser = function(url, callback) {
superagent.get(url, callback);
};
/**
* Get auth info for a given username
*
* @param {string} domain - Domain which hosts the user's info
* @param {string} username - Username who's info we are retreiving
* @param {function} fn - Callback function
*/
AuthInfo.get = function(domain, username, callback) {
var self = this;
username = username.toLowerCase();
function getRippleTxt(callback) {
self._getRippleTxt(domain, function(err, txt) {
if (err) {
return callback(err);
}
if (!txt.authinfo_url) {
return callback(new Error('Authentication is not supported on ' + domain));
}
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += '?domain=' + domain + '&username=' + username;
callback(null, url);
});
};
function getUser(url, callback) {
self._getUser(url, function(err, res) {
if (err || res.error) {
callback(new Error('Authentication info server unreachable'));
} else {
callback(null, res.body);
}
});
};
async.waterfall([ getRippleTxt, getUser ], callback);
};
exports.AuthInfo = AuthInfo;

View File

@@ -7,9 +7,9 @@ var BigInteger = utils.jsbn.BigInteger;
var Base = {};
var alphabets = Base.alphabets = {
ripple : "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz",
tipple : "RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz",
bitcoin : "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
ripple: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz',
tipple: 'RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz',
bitcoin: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
};
extend(Base, {
@@ -33,7 +33,7 @@ function sha256hash(bytes) {
// --> input: big-endian array of bytes.
// <-- string at least as long as input.
Base.encode = function (input, alpha) {
Base.encode = function(input, alpha) {
var alphabet = alphabets[alpha || 'ripple'];
var bi_base = new BigInteger(String(alphabet.length));
var bi_q = new BigInteger();
@@ -56,7 +56,7 @@ Base.encode = function (input, alpha) {
// --> input: String
// <-- array of bytes or undefined.
Base.decode = function (input, alpha) {
Base.decode = function(input, alpha) {
if (typeof input !== 'string') {
return void(0);
}
@@ -66,8 +66,8 @@ Base.decode = function (input, alpha) {
var bi_value = new BigInteger();
var i;
for (i = 0; i != input.length && input[i] === alphabet[0]; i += 1)
;
for (i = 0; i !== input.length && input[i] === alphabet[0]; i += 1) {
}
for (; i !== input.length; i += 1) {
var v = alphabet.indexOf(input[i]);
@@ -84,10 +84,10 @@ Base.decode = function (input, alpha) {
// toByteArray:
// - Returns leading zeros!
// - Returns signed bytes!
var bytes = bi_value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0; });
var bytes = bi_value.toByteArray().map(function(b) { return b ? b < 0 ? 256+b : b : 0; });
var extra = 0;
while (extra != bytes.length && !bytes[extra]) {
while (extra !== bytes.length && !bytes[extra]) {
extra += 1;
}
@@ -104,7 +104,7 @@ Base.decode = function (input, alpha) {
return [].concat(utils.arraySet(zeros, 0), bytes);
};
Base.verify_checksum = function (bytes) {
Base.verify_checksum = function(bytes) {
var computed = sha256hash(bytes.slice(0, -4)).slice(0, 4);
var checksum = bytes.slice(-4);
var result = true;
@@ -121,7 +121,7 @@ Base.verify_checksum = function (bytes) {
// --> input: Array
// <-- String
Base.encode_check = function (version, input, alphabet) {
Base.encode_check = function(version, input, alphabet) {
var buffer = [].concat(version, input);
var check = sha256(sha256(buffer)).slice(0, 4);
@@ -130,7 +130,7 @@ Base.encode_check = function (version, input, alphabet) {
// --> input : String
// <-- NaN || BigInteger
Base.decode_check = function (version, input, alphabet) {
Base.decode_check = function(version, input, alphabet) {
var buffer = Base.decode(input, alphabet);
if (!buffer || buffer.length < 5) {

View File

@@ -208,6 +208,7 @@ var base = [
[ 'TransactionType' , REQUIRED ],
[ 'Flags' , OPTIONAL ],
[ 'SourceTag' , OPTIONAL ],
[ 'LastLedgerSequence' , OPTIONAL ],
[ 'Account' , REQUIRED ],
[ 'Sequence' , REQUIRED ],
[ 'Fee' , REQUIRED ],
@@ -274,133 +275,105 @@ exports.tx = {
])
};
exports.ledger = {
AccountRoot: [97],
Contract: [99],
DirectoryNode: [100],
Features: [102],
GeneratorMap: [103],
LedgerHashes: [104],
Nickname: [110],
Offer: [111],
RippleState: [114],
FeeSettings: [115]
};
/*
TODO:
Need `base` factored out
AccountRoot needs AccountTxnID
var sleBase = [
['LedgerIndex', OPTIONAL],
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED]
];
{
'AccountRoot': [97,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['Sequence', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['TransferRate', OPTIONAL],
['WalletSize', OPTIONAL],
['OwnerCount', REQUIRED],
['EmailHash', OPTIONAL],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['WalletLocator', OPTIONAL],
['Balance', REQUIRED],
['MessageKey', OPTIONAL,],
['Domain', OPTIONAL,],
['Account', REQUIRED],
['RegularKey', OPTIONAL]],
'Contract': [99,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['Expiration', REQUIRED],
['BondAmount', REQUIRED],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['Balance', REQUIRED],
['FundCode', OPTIONAL],
['RemoveCode', OPTIONAL],
['ExpireCode', OPTIONAL],
['CreateCode', OPTIONAL],
['Account', REQUIRED],
['Owner', REQUIRED],
['Issuer', REQUIRED]],
'DirectoryNode': [100,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['IndexNext', OPTIONAL],
['IndexPrevious', OPTIONAL],
['ExchangeRate', OPTIONAL],
['LedgerIndex', OPTIONAL],
['RootIndex', REQUIRED],
['Owner', OPTIONAL],
['TakerPaysCurrency', OPTIONAL],
['TakerPaysIssuer', OPTIONAL],
['TakerGetsCurrency', OPTIONAL],
['TakerGetsIssuer', OPTIONAL],
['Indexes', REQUIRED]],
'EnabledFeatures': [102,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['LedgerIndex', OPTIONAL],
['Features', REQUIRED]],
'FeeSettings': [115,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['ReferenceFeeUnits', REQUIRED],
['ReserveBase', REQUIRED],
['ReserveIncrement', REQUIRED],
['BaseFee', REQUIRED],
['LedgerIndex', OPTIONAL]],
'GeneratorMap': [103,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['LedgerIndex', OPTIONAL],
['Generator', REQUIRED,]],
'LedgerHashes': [104,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['FirstLedgerSequence', OPTIONAL],
['LastLedgerSequence', OPTIONAL],
['LedgerIndex', OPTIONAL],
['Hashes', REQUIRED]],
'Nickname': [110,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['LedgerIndex', OPTIONAL],
['MinimumOffer', OPTIONAL],
['Account', REQUIRED]],
'Offer': [111,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['Sequence', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['Expiration', OPTIONAL],
['BookNode', REQUIRED],
['OwnerNode', REQUIRED],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['BookDirectory', REQUIRED],
['TakerPays', REQUIRED],
['TakerGets', REQUIRED],
['Account', REQUIRED]],
'RippleState': [114,
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['HighQualityIn', OPTIONAL],
['HighQualityOut', OPTIONAL],
['LowQualityIn', OPTIONAL],
['LowQualityOut', OPTIONAL],
['LowNode', OPTIONAL],
['HighNode', OPTIONAL],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['Balance', REQUIRED],
['LowLimit', REQUIRED],
['HighLimit', REQUIRED]]
exports.ledger = {
AccountRoot: [97].concat(sleBase,[
['Sequence', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['TransferRate', OPTIONAL],
['WalletSize', OPTIONAL],
['OwnerCount', REQUIRED],
['EmailHash', OPTIONAL],
['PreviousTxnID', REQUIRED],
['AccountTxnID', OPTIONAL],
['WalletLocator', OPTIONAL],
['Balance', REQUIRED],
['MessageKey', OPTIONAL],
['Domain', OPTIONAL],
['Account', REQUIRED],
['RegularKey', OPTIONAL]]),
Contract: [99].concat(sleBase,[
['PreviousTxnLgrSeq', REQUIRED],
['Expiration', REQUIRED],
['BondAmount', REQUIRED],
['PreviousTxnID', REQUIRED],
['Balance', REQUIRED],
['FundCode', OPTIONAL],
['RemoveCode', OPTIONAL],
['ExpireCode', OPTIONAL],
['CreateCode', OPTIONAL],
['Account', REQUIRED],
['Owner', REQUIRED],
['Issuer', REQUIRED]]),
DirectoryNode: [100].concat(sleBase,[
['IndexNext', OPTIONAL],
['IndexPrevious', OPTIONAL],
['ExchangeRate', OPTIONAL],
['RootIndex', REQUIRED],
['Owner', OPTIONAL],
['TakerPaysCurrency', OPTIONAL],
['TakerPaysIssuer', OPTIONAL],
['TakerGetsCurrency', OPTIONAL],
['TakerGetsIssuer', OPTIONAL],
['Indexes', REQUIRED]]),
EnabledFeatures: [102].concat(sleBase,[
['Features', REQUIRED]]),
FeeSettings: [115].concat(sleBase,[
['ReferenceFeeUnits', REQUIRED],
['ReserveBase', REQUIRED],
['ReserveIncrement', REQUIRED],
['BaseFee', REQUIRED],
['LedgerIndex', OPTIONAL]]),
GeneratorMap: [103].concat(sleBase,[
['Generator', REQUIRED]]),
LedgerHashes: [104].concat(sleBase,[
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['FirstLedgerSequence', OPTIONAL],
['LastLedgerSequence', OPTIONAL],
['LedgerIndex', OPTIONAL],
['Hashes', REQUIRED]]),
Nickname: [110].concat(sleBase,[
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['LedgerIndex', OPTIONAL],
['MinimumOffer', OPTIONAL],
['Account', REQUIRED]]),
Offer: [111].concat(sleBase,[
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['Sequence', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['Expiration', OPTIONAL],
['BookNode', REQUIRED],
['OwnerNode', REQUIRED],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['BookDirectory', REQUIRED],
['TakerPays', REQUIRED],
['TakerGets', REQUIRED],
['Account', REQUIRED]]),
RippleState: [114].concat(sleBase,[
['LedgerEntryType', REQUIRED],
['Flags', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED],
['HighQualityIn', OPTIONAL],
['HighQualityOut', OPTIONAL],
['LowQualityIn', OPTIONAL],
['LowQualityOut', OPTIONAL],
['LowNode', OPTIONAL],
['HighNode', OPTIONAL],
['PreviousTxnID', REQUIRED],
['LedgerIndex', OPTIONAL],
['Balance', REQUIRED],
['LowLimit', REQUIRED],
['HighLimit', REQUIRED]])
}
*/
exports.metadata = [
[ 'TransactionIndex' , REQUIRED ],

1174
src/js/ripple/blob.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
// This object serves as a singleton to store config options
var extend = require("extend");
var extend = require('extend');
var config = module.exports = {
load: function (newOpts) {

313
src/js/ripple/crypt.js Normal file
View File

@@ -0,0 +1,313 @@
var sjcl = require('./utils').sjcl;
var base = require('./base').Base;
var Seed = require('./seed').Seed;
var UInt160 = require('./uint160').UInt160;
var UInt256 = require('./uint256').UInt256;
var request = require('superagent');
var querystring = require('querystring');
var extend = require("extend");
var parser = require("url");
var Crypt = { };
var cryptConfig = {
cipher : 'aes',
mode : 'ccm',
ts : 64, // tag length
ks : 256, // key size
iter : 1000 // iterations (key derivation)
};
/**
* Full domain hash based on SHA512
*/
function fdh(data, bytelen) {
var bitlen = bytelen << 3;
if (typeof data === 'string') {
data = sjcl.codec.utf8String.toBits(data);
}
// Add hashing rounds until we exceed desired length in bits
var counter = 0, output = [];
while (sjcl.bitArray.bitLength(output) < bitlen) {
var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data));
output = sjcl.bitArray.concat(output, hash);
counter++;
}
// Truncate to desired length
output = sjcl.bitArray.clamp(output, bitlen);
return output;
};
/**
* This is a function to derive different hashes from the same key.
* Each hash is derived as HMAC-SHA512HALF(key, token).
*
* @param {string} key
* @param {string} hash
*/
function keyHash(key, token) {
var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256));
};
/****** exposed functions ******/
/**
* KEY DERIVATION FUNCTION
*
* This service takes care of the key derivation, i.e. converting low-entropy
* secret into higher entropy secret via either computationally expensive
* processes or peer-assisted key derivation (PAKDF).
*
* @param {object} opts
* @param {string} purpose - Key type/purpose
* @param {string} username
* @param {string} secret - Also known as passphrase/password
* @param {function} fn
*/
Crypt.derive = function(opts, purpose, username, secret, fn) {
var tokens;
if (purpose === 'login') {
tokens = ['id', 'crypt'];
} else {
tokens = ['unlock'];
}
var iExponent = new sjcl.bn(String(opts.exponent));
var iModulus = new sjcl.bn(String(opts.modulus));
var iAlpha = new sjcl.bn(String(opts.alpha));
var publicInfo = [ 'PAKDF_1_0_0', opts.host.length, opts.host, username.length, username, purpose.length, purpose ].join(':') + ':';
var publicSize = Math.ceil(Math.min((7 + iModulus.bitLength()) >>> 3, 256) / 8);
var publicHash = fdh(publicInfo, publicSize);
var publicHex = sjcl.codec.hex.fromBits(publicHash);
var iPublic = new sjcl.bn(String(publicHex)).setBitM(0);
var secretInfo = [ publicInfo, secret.length, secret ].join(':') + ':';
var secretSize = (7 + iModulus.bitLength()) >>> 3;
var secretHash = fdh(secretInfo, secretSize);
var secretHex = sjcl.codec.hex.fromBits(secretHash);
var iSecret = new sjcl.bn(String(secretHex)).mod(iModulus);
if (iSecret.jacobi(iModulus) !== 1) {
iSecret = iSecret.mul(iAlpha).mod(iModulus);
}
var iRandom;
for (;;) {
iRandom = sjcl.bn.random(iModulus, 0);
if (iRandom.jacobi(iModulus) === 1) {
break;
}
}
var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus);
var iSignreq = iSecret.mulmod(iBlind, iModulus);
var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits());
request.post(opts.url)
.send({ info: publicInfo, signreq: signreq })
.end(function(err, resp) {
if (err || !resp) {
return fn(new Error('Could not query PAKDF server ' + opts.host));
}
var data = resp.body || resp.text ? JSON.parse(resp.text) : {};
if (data.result !== 'success') {
return fn(new Error('Could not query PAKDF server '+opts.host));
}
var iSignres = new sjcl.bn(String(data.signres));
var iRandomInv = iRandom.inverseMod(iModulus);
var iSigned = iSignres.mulmod(iRandomInv, iModulus);
var key = iSigned.toBits();
var result = { };
tokens.forEach(function(token) {
result[token] = keyHash(key, token);
});
fn(null, result);
});
};
/**
* Imported from ripple-client
*/
/**
* Encrypt data
*
* @param {string} key
* @param {string} data
*/
Crypt.encrypt = function(key, data) {
key = sjcl.codec.hex.toBits(key);
var opts = extend(true, {}, cryptConfig);
var encryptedObj = JSON.parse(sjcl.encrypt(key, data, opts));
var version = [sjcl.bitArray.partial(8, 0)];
var initVector = sjcl.codec.base64.toBits(encryptedObj.iv);
var ciphertext = sjcl.codec.base64.toBits(encryptedObj.ct);
var encryptedBits = sjcl.bitArray.concat(version, initVector);
encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext);
return sjcl.codec.base64.fromBits(encryptedBits);
};
/**
* Decrypt data
*
* @param {string} key
* @param {string} data
*/
Crypt.decrypt = function (key, data) {
key = sjcl.codec.hex.toBits(key);
var encryptedBits = sjcl.codec.base64.toBits(data);
var version = sjcl.bitArray.extract(encryptedBits, 0, 8);
if (version !== 0) {
throw new Error('Unsupported encryption version: '+version);
}
var encrypted = extend(true, {}, cryptConfig, {
iv: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8, 8+128)),
ct: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8+128))
});
return sjcl.decrypt(key, JSON.stringify(encrypted));
};
/**
* Validate a ripple address
*
* @param {string} address
*/
Crypt.isValidAddress = function (address) {
return UInt160.is_valid(address);
};
/**
* Create an encryption key
*
* @param {integer} nWords - number of words
*/
Crypt.createSecret = function (nWords) {
return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords));
};
/**
* Create a new master key
*/
Crypt.createMaster = function () {
return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4)));
};
/**
* Create a ripple address from a master key
*
* @param {string} masterkey
*/
Crypt.getAddress = function (masterkey) {
return Seed.from_json(masterkey).get_key().get_address().to_json();
};
/**
* Hash data using SHA-512.
*
* @param {string|bitArray} data
* @return {string} Hash of the data
*/
Crypt.hashSha512 = function (data) {
// XXX Should return a UInt512
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
};
/**
* Hash data using SHA-512 and return the first 256 bits.
*
* @param {string|bitArray} data
* @return {UInt256} Hash of the data
*/
Crypt.hashSha512Half = function (data) {
return UInt256.from_hex(Crypt.hashSha512(data).substr(0, 64));
};
/**
* Sign a data string with a secret key
*
* @param {string} secret
* @param {string} data
*/
Crypt.signString = function(secret, data) {
var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(hmac.mac(data));
};
/**
* Create an an accout recovery key
*
* @param {string} secret
*/
Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) {
var seed = Seed.from_json(secret).to_bits();
var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512);
var key = hmac.mac('ripple/hmac/recovery_encryption_key/v1');
key = sjcl.bitArray.bitSlice(key, 0, 256);
return sjcl.codec.hex.fromBits(key);
};
/**
* Convert base64 encoded data into base64url encoded data.
*
* @param {String} base64 Data
*/
Crypt.base64ToBase64Url = function(encodedData) {
return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, '');
};
/**
* Convert base64url encoded data into base64 encoded data.
*
* @param {String} base64 Data
*/
Crypt.base64UrlToBase64 = function(encodedData) {
encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/');
while (encodedData.length % 4) {
encodedData += '=';
}
return encodedData;
};
exports.Crypt = Crypt;

View File

@@ -1,15 +1,13 @@
var extend = require('extend');
var UInt160 = require('./uint160').UInt160;
var Float = require('./float').Float;
var utils = require('./utils');
var Float = require('./ieee754').Float;
//
// Currency support
//
var Currency = extend(function () {
var Currency = extend(function() {
// Internal form: 0 = XRP. 3 letter-code.
// XXX Internal should be 0 or hex with three letter annotation when valid.
@@ -26,9 +24,35 @@ var Currency = extend(function () {
Currency.prototype = extend({}, UInt160.prototype);
Currency.prototype.constructor = Currency;
Currency.HEX_CURRENCY_BAD = "0000000000000000000000005852500000000000";
Currency.HEX_CURRENCY_BAD = '0000000000000000000000005852500000000000';
Currency.from_json = function (j, shouldInterpretXrpAsIou) {
/**
* Tries to correctly interpret a Currency as entered by a user.
*
* Examples:
*
* USD => currency
* USD - Dollar => currency with optional full currency name
* XAU (-0.5%pa) => XAU with 0.5% effective demurrage rate per year
* XAU - Gold (-0.5%pa) => Optionally allowed full currency name
* USD (1%pa) => US dollars with 1% effective interest per year
* INR - Indian Rupees => Optional full currency name with spaces
* TYX - 30-Year Treasuries => Optional full currency with numbers and a dash
* TYX - 30-Year Treasuries (1.5%pa) => Optional full currency with numbers, dash and interest rate
*
* The regular expression below matches above cases, broken down for better understanding:
*
* ^\s* // start with any amount of whitespace
* ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3 digit numeric currency-code. See ISO 4217
* (\s*-\s*[- \w]+) // optional full currency name following the dash after currency code,
* full currency code can contain letters, numbers and dashes
* (\s*\(-?\d+\.?\d*%pa\))? // optional demurrage rate, has optional - and . notation (-0.5%pa)
* \s*$ // end with any amount of whitespace
*
*/
Currency.prototype.human_RE = /^\s*([a-zA-Z]{3}|[0-9]{3})(\s*-\s*[- \w]+)?(\s*\(-?\d+\.?\d*%pa\))?\s*$/;
Currency.from_json = function(j, shouldInterpretXrpAsIou) {
if (j instanceof this) {
return j.clone();
} else {
@@ -36,24 +60,85 @@ Currency.from_json = function (j, shouldInterpretXrpAsIou) {
}
};
Currency.from_human = function(j, opts) {
return (new Currency().parse_human(j, opts));
}
// this._value = NaN on error.
Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
this._value = NaN;
switch (typeof j) {
case 'string':
if (!j || /^(0|XRP)$/.test(j)) {
if (shouldInterpretXrpAsIou) {
this.parse_hex(Currency.HEX_CURRENCY_BAD);
} else {
this.parse_hex(Currency.HEX_ZERO);
}
} else if (/^[a-zA-Z0-9]{3}$/.test(j)) {
var currencyCode = j.toUpperCase();
break;
}
// match the given string to see if it's in an allowed format
var matches = String(j).match(this.human_RE);
if (matches) {
var currencyCode = matches[1];
// the full currency is matched as it is part of the valid currency format, but not stored
// var full_currency = matches[2] || '';
var interest = matches[3] || '';
// interest is defined as interest per year, per annum (pa)
var percentage = interest.match(/(-?\d+\.?\d+)/);
currencyCode = currencyCode.toUpperCase();
var currencyData = utils.arraySet(20, 0);
currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
currencyData[13] = currencyCode.charCodeAt(1) & 0xff;
currencyData[14] = currencyCode.charCodeAt(2) & 0xff;
if (percentage) {
/*
* 20 byte layout of a interest bearing currency
*
* 01 __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __
* CURCODE- DATE------- RATE------------------- RESERVED---
*/
// byte 1 for type, use '1' to denote demurrage currency
currencyData[0] = 1;
// byte 2-4 for currency code
currencyData[1] = currencyCode.charCodeAt(0) & 0xff;
currencyData[2] = currencyCode.charCodeAt(1) & 0xff;
currencyData[3] = currencyCode.charCodeAt(2) & 0xff;
// byte 5-8 are for reference date, but should always be 0 so we won't fill it
// byte 9-16 are for the interest
percentage = parseFloat(percentage[0]);
// the interest or demurrage is expressed as a yearly (per annum) value
var secondsPerYear = 31536000; // 60 * 60 * 24 * 365
// Calculating the interest e-fold
// 0.5% demurrage is expressed 0.995, 0.005 less than 1
// 0.5% interest is expressed as 1.005, 0.005 more than 1
var interestEfold = secondsPerYear / Math.log(1 + percentage/100);
var bytes = Float.toIEEE754Double(interestEfold);
for (var i=0; i<=bytes.length; i++) {
currencyData[8 + i] = bytes[i] & 0xff;
}
// the last 4 bytes are reserved for future use, so we won't fill those
} else {
currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
currencyData[13] = currencyCode.charCodeAt(1) & 0xff;
currencyData[14] = currencyCode.charCodeAt(2) & 0xff;
}
this.parse_bytes(currencyData);
} else {
this.parse_hex(j);
@@ -77,24 +162,29 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
return this;
};
Currency.prototype.parse_human = function(j) {
return this.parse_json(j);
};
/**
* Recalculate internal representation.
*
* You should never need to call this.
*/
Currency.prototype._update = function () {
Currency.prototype._update = function() {
var bytes = this.to_bytes();
// is it 0 everywhere except 12, 13, 14?
var isZeroExceptInStandardPositions = true;
if (!bytes) {
return "XRP";
return 'XRP';
}
this._native = false;
this._type = -1;
this._interest_start = new Date();
this._interest_start = NaN;
this._interest_period = NaN;
this._iso_code = '';
@@ -107,9 +197,9 @@ Currency.prototype._update = function () {
+ String.fromCharCode(bytes[13])
+ String.fromCharCode(bytes[14]);
if (this._iso_code === "\0\0\0") {
if (this._iso_code === '\0\0\0') {
this._native = true;
this._iso_code = "XRP";
this._iso_code = 'XRP';
}
this._type = 0;
@@ -123,13 +213,13 @@ Currency.prototype._update = function () {
(bytes[5] << 16) +
(bytes[6] << 8) +
(bytes[7] );
this._interest_period = Float.fromBytes(bytes.slice(8, 16));
this._interest_period = Float.fromIEEE754Double(bytes.slice(8, 16));
}
};
// XXX Probably not needed anymore?
/*
Currency.prototype.parse_bytes = function (byte_array) {
Currency.prototype.parse_bytes = function(byte_array) {
if (Array.isArray(byte_array) && byte_array.length === 20) {
var result;
// is it 0 everywhere except 12, 13, 14?
@@ -143,9 +233,9 @@ Currency.prototype.parse_bytes = function (byte_array) {
var currencyCode = String.fromCharCode(byte_array[12])
+ String.fromCharCode(byte_array[13])
+ String.fromCharCode(byte_array[14]);
if (/^[A-Z0-9]{3}$/.test(currencyCode) && currencyCode !== "XRP" ) {
if (/^[A-Z0-9]{3}$/.test(currencyCode) && currencyCode !== 'XRP' ) {
this._value = currencyCode;
} else if (currencyCode === "\0\0\0") {
} else if (currencyCode === '\0\0\0') {
this._value = 0;
} else {
this._value = NaN;
@@ -161,25 +251,51 @@ Currency.prototype.parse_bytes = function (byte_array) {
};
*/
Currency.prototype.is_native = function () {
Currency.prototype.is_native = function() {
return this._native;
};
/**
* Whether this currency is an interest-bearing/demurring currency.
*/
Currency.prototype.has_interest = function () {
return this._type === 1 && this._interest_start && !isNaN(this._interest_period);
Currency.prototype.has_interest = function() {
return this._type === 1 && !isNaN(this._interest_start) && !isNaN(this._interest_period);
};
Currency.prototype.get_interest_at = function (referenceDate) {
if (!this.has_interest) return 1;
/**
*
* @param referenceDate - number of seconds since the Ripple Epoch (0:00 on January 1, 2000 UTC)
* used to calculate the interest over provided interval
* pass in one years worth of seconds to ge the yearly interest
* @returns {number} - interest for provided interval, can be negative for demurred currencies
*/
Currency.prototype.get_interest_at = function(referenceDate, decimals) {
if (!this.has_interest) {
return 0;
}
// use one year as a default period
if (!referenceDate) {
referenceDate = this._interest_start + 3600 * 24 * 365;
}
if (referenceDate instanceof Date) {
referenceDate = utils.fromTimestamp(referenceDate.getTime());
}
return Math.pow(Math.E, (referenceDate - this._interest_start) / this._interest_period);
// calculate interest by e-fold number
return Math.exp((referenceDate - this._interest_start) / this._interest_period);
};
Currency.prototype.get_interest_percentage_at = function(referenceDate, decimals) {
var interest = this.get_interest_at(referenceDate, decimals);
// convert to percentage
var interest = (interest*100)-100;
var decimalMultiplier = decimals ? Math.pow(10,decimals) : 100;
// round to two decimals behind the dot
return Math.round(interest*decimalMultiplier) / decimalMultiplier;
};
// XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine.
@@ -187,42 +303,51 @@ Currency.prototype.get_interest_at = function (referenceDate) {
// We could be doing further checks into the internal format of the
// currency data, since there are some values that are invalid.
//
//Currency.prototype.is_valid = function () {
//Currency.prototype.is_valid = function() {
// return this._value instanceof BigInteger && ...;
//};
Currency.prototype.to_json = function () {
Currency.prototype.to_json = function(opts) {
if (!this.is_valid()) {
// XXX This is backwards compatible behavior, but probably not very good.
return "XRP";
return 'XRP';
}
var currency;
var fullName = opts && opts.full_name ? " - " + opts.full_name : "";
// Any currency with standard properties and a valid code can be abbreviated
// in the JSON wire format as the three character code.
if (/^[A-Z0-9]{3}$/.test(this._iso_code) && !this.has_interest()) {
return this._iso_code;
currency = this._iso_code + fullName;
// If there is interest, append the annual interest to the full currency name
} else if (this.has_interest()) {
var decimals = opts ? opts.decimals : undefined;
currency = this._iso_code + fullName + " (" + this.get_interest_percentage_at(this._interest_start + 3600 * 24 * 365, decimals) + "%pa)";
} else {
// Fallback to returning the raw currency hex
currency = this.to_hex();
// XXX This is to maintain backwards compatibility, but it is very, very odd
// behavior, so we should deprecate it and get rid of it as soon as
// possible.
if (currency === Currency.HEX_ONE) {
currency = 1;
}
}
// Fallback to returning the raw currency hex
var currencyHex = this.to_hex();
// XXX This is to maintain backwards compatibility, but it is very, very odd
// behavior, so we should deprecate it and get rid of it as soon as
// possible.
if (currencyHex === Currency.HEX_ONE) {
return 1;
}
return currencyHex;
return currency;
};
Currency.prototype.to_human = function () {
Currency.prototype.to_human = function(opts) {
// to_human() will always print the human-readable currency code if available.
if (/^[A-Z0-9]{3}$/.test(this._iso_code)) {
return this._iso_code;
}
return this.to_json(opts);
};
return this.to_json();
Currency.prototype.get_iso = function() {
return this._iso_code;
};
exports.Currency = Currency;

View File

@@ -8,12 +8,17 @@ var Float = exports.Float = {};
var allZeros = /^0+$/;
var allOnes = /^1+$/;
Float.fromBytes = function (bytes) {
Float.fromBytes = function(bytes) {
// Render in binary. Hackish.
var b = "";
var b = '';
for (var i = 0, n = bytes.length; i < n; i++) {
var bits = (bytes[i] & 0xff).toString(2);
while (bits.length < 8) bits = "0" + bits;
while (bits.length < 8) {
bits = '0' + bits;
}
b += bits;
}
@@ -29,13 +34,11 @@ Float.fromBytes = function (bytes) {
var m = b.substring(exponentBits + 1);
var value = 0;
var multiplier = (s === "0" ? 1 : -1);
var multiplier = (s === '0' ? 1 : -1);
if (allZeros.test(e)) {
// Zero or denormalized
if (allZeros.test(m)) {
// Value is zero
} else {
if (!allZeros.test(m)) {
value = parseInt(m, 2) * Math.pow(2, minExponent);
}
} else if (allOnes.test(e)) {

View File

@@ -17,6 +17,8 @@ exports.HASH_TX_ID = 0x54584E00; // 'TXN'
exports.HASH_TX_NODE = 0x534E4400; // 'TND'
// inner node in tree
exports.HASH_INNER_NODE = 0x4D494E00; // 'MIN'
// leaf node in tree
exports.HASH_LEAF_NODE = 0x4D4C4E00; // 'MLN'
// inner transaction to sign
exports.HASH_TX_SIGN = 0x53545800; // 'STX'
// inner transaction to sign (TESTNET)

107
src/js/ripple/ieee754.js Normal file
View File

@@ -0,0 +1,107 @@
// Convert a JavaScript number to IEEE-754 Double Precision
// value represented as an array of 8 bytes (octets)
//
// Based on:
// http://cautionsingularityahead.blogspot.com/2010/04/javascript-and-ieee754-redux.html
//
// Found and modified from:
// https://gist.github.com/bartaz/1119041
var Float = exports.Float = {};
Float.toIEEE754 = function(v, ebits, fbits) {
var bias = (1 << (ebits - 1)) - 1;
// Compute sign, exponent, fraction
var s, e, f;
if (isNaN(v)) {
e = (1 << bias) - 1; f = 1; s = 0;
}
else if (v === Infinity || v === -Infinity) {
e = (1 << bias) - 1; f = 0; s = (v < 0) ? 1 : 0;
}
else if (v === 0) {
e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0;
}
else {
s = v < 0;
v = Math.abs(v);
if (v >= Math.pow(2, 1 - bias)) {
var ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
e = ln + bias;
f = v * Math.pow(2, fbits - ln) - Math.pow(2, fbits);
}
else {
e = 0;
f = v / Math.pow(2, 1 - bias - fbits);
}
}
// Pack sign, exponent, fraction
var i, bits = [];
for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = Math.floor(f / 2); }
for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = Math.floor(e / 2); }
bits.push(s ? 1 : 0);
bits.reverse();
var str = bits.join('');
// Bits to bytes
var bytes = [];
while (str.length) {
bytes.push(parseInt(str.substring(0, 8), 2));
str = str.substring(8);
}
return bytes;
}
Float.fromIEEE754 = function(bytes, ebits, fbits) {
// Bytes to bits
var bits = [];
for (var i = bytes.length; i; i -= 1) {
var byte = bytes[i - 1];
for (var j = 8; j; j -= 1) {
bits.push(byte % 2 ? 1 : 0); byte = byte >> 1;
}
}
bits.reverse();
var str = bits.join('');
// Unpack sign, exponent, fraction
var bias = (1 << (ebits - 1)) - 1;
var s = parseInt(str.substring(0, 1), 2) ? -1 : 1;
var e = parseInt(str.substring(1, 1 + ebits), 2);
var f = parseInt(str.substring(1 + ebits), 2);
// Produce number
if (e === (1 << ebits) - 1) {
return f !== 0 ? NaN : s * Infinity;
}
else if (e > 0) {
return s * Math.pow(2, e - bias) * (1 + f / Math.pow(2, fbits));
}
else if (f !== 0) {
return s * Math.pow(2, -(bias-1)) * (f / Math.pow(2, fbits));
}
else {
return s * 0;
}
}
Float.fromIEEE754Double = function(b) { return Float.fromIEEE754(b, 11, 52); }
Float.toIEEE754Double = function(v) { return Float.toIEEE754(v, 11, 52); }
Float.fromIEEE754Single = function(b) { return Float.fromIEEE754(b, 8, 23); }
Float.toIEEE754Single = function(v) { return Float.toIEEE754(v, 8, 23); }
// Convert array of octets to string binary representation
// by bartaz
Float.toIEEE754DoubleString = function(v) {
return exports.toIEEE754Double(v)
.map(function(n){ for(n = n.toString(2);n.length < 8;n="0"+n); return n })
.join('')
.replace(/(.)(.{11})(.{52})/, "$1 $2 $3")
}

View File

@@ -11,7 +11,10 @@ exports.Seed = require('./seed').Seed;
exports.Meta = require('./meta').Meta;
exports.SerializedObject = require('./serializedobject').SerializedObject;
exports.RippleError = require('./rippleerror').RippleError;
exports.Message = require('./message').Message;
exports.VaultClient = require('./vaultclient').VaultClient;
exports.AuthInfo = require('./authinfo').AuthInfo;
exports.RippleTxt = require('./rippletxt').RippleTxt;
exports.binformat = require('./binformat');
exports.utils = require('./utils');
exports.Server = require('./server').Server;
@@ -34,7 +37,9 @@ function attachUnderscored(c) {
Object.keys(o.prototype).forEach(function(key) {
var UPPERCASE = /([A-Z]{1})[a-z]+/g;
if (!UPPERCASE.test(key)) return;
if (!UPPERCASE.test(key)) {
return;
}
var underscored = key.replace(UPPERCASE, function(c) {
return '_' + c.toLowerCase();

View File

@@ -5,17 +5,17 @@ var UInt256 = require('./uint256').UInt256;
var Base = require('./base').Base;
function KeyPair() {
this._curve = sjcl.ecc.curves['c256'];
this._curve = sjcl.ecc.curves.c256;
this._secret = null;
this._pubkey = null;
};
KeyPair.from_bn_secret = function (j) {
return j instanceof this ? j.clone() : (new this()).parse_bn_secret(j);
KeyPair.from_bn_secret = function(j) {
return (j instanceof this) ? j.clone() : (new this()).parse_bn_secret(j);
};
KeyPair.prototype.parse_bn_secret = function (j) {
this._secret = new sjcl.ecc.ecdsa.secretKey(sjcl.ecc.curves['c256'], j);
KeyPair.prototype.parse_bn_secret = function(j) {
this._secret = new sjcl.ecc.ecdsa.secretKey(sjcl.ecc.curves.c256, j);
return this;
};
@@ -24,7 +24,7 @@ KeyPair.prototype.parse_bn_secret = function (j) {
*
* @private
*/
KeyPair.prototype._pub = function () {
KeyPair.prototype._pub = function() {
var curve = this._curve;
if (!this._pubkey && this._secret) {
@@ -40,7 +40,7 @@ KeyPair.prototype._pub = function () {
*
* @private
*/
KeyPair.prototype._pub_bits = function () {
KeyPair.prototype._pub_bits = function() {
var pub = this._pub();
if (!pub) {
@@ -60,7 +60,7 @@ KeyPair.prototype._pub_bits = function () {
*
* Key will be returned as a compressed pubkey - 33 bytes converted to hex.
*/
KeyPair.prototype.to_hex_pub = function () {
KeyPair.prototype.to_hex_pub = function() {
var bits = this._pub_bits();
if (!bits) {
@@ -74,7 +74,7 @@ function SHA256_RIPEMD160(bits) {
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
}
KeyPair.prototype.get_address = function () {
KeyPair.prototype.get_address = function() {
var bits = this._pub_bits();
if (!bits) {
@@ -88,9 +88,11 @@ KeyPair.prototype.get_address = function () {
return address;
};
KeyPair.prototype.sign = function (hash) {
var hash = UInt256.from_json(hash);
return this._secret.signDER(hash.to_bits(), 0);
KeyPair.prototype.sign = function(hash) {
hash = UInt256.from_json(hash);
var sig = this._secret.sign(hash.to_bits(), 0);
sig = this._secret.canonicalizeSignature(sig);
return this._secret.encodeDER(sig);
};
exports.KeyPair = KeyPair;

View File

@@ -5,6 +5,11 @@ var SHAMap = require('./shamap').SHAMap;
var SHAMapTreeNode = require('./shamap').SHAMapTreeNode;
var SerializedObject = require('./serializedobject').SerializedObject;
var stypes = require('./serializedtypes');
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var stypes = require('./serializedtypes');
var sjcl = require('./utils').sjcl;
var Crypt = require('./crypt').Crypt;
function Ledger()
{
@@ -17,6 +22,91 @@ Ledger.from_json = function (v) {
return ledger;
};
Ledger.space = require('./ledgerspaces');
/**
* Generate the key for an AccountRoot entry.
*
* @param {String|UInt160} account Ripple Account
* @return {UInt256}
*/
Ledger.calcAccountRootEntryHash =
Ledger.prototype.calcAccountRootEntryHash = function (account) {
account = UInt160.from_json(account);
var index = new SerializedObject();
index.append([0, Ledger.space.account.charCodeAt(0)]);
index.append(account.to_bytes());
return index.hash();
};
/**
* Generate the key for an Offer entry.
*
* @param {String|UInt160} account Ripple Account
* @param {Number} sequence Sequence number of the OfferCreate transaction
* that instantiated this offer.
* @return {UInt256}
*/
Ledger.calcOfferEntryHash =
Ledger.prototype.calcOfferEntryHash = function (account, sequence) {
account = UInt160.from_json(account);
sequence = parseInt(sequence);
var index = new SerializedObject();
index.append([0, Ledger.space.offer.charCodeAt(0)]);
index.append(account.to_bytes());
stypes.Int32.serialize(index, sequence);
return index.hash();
};
/**
* Generate the key for a RippleState entry.
*
* The ordering of the two account parameters does not matter.
*
* @param {String|UInt160} account1 First Ripple Account
* @param {String|UInt160} account2 Second Ripple Account
* @param {String|Currency} currency The currency code
* @return {UInt256}
*/
Ledger.calcRippleStateEntryHash =
Ledger.prototype.calcRippleStateEntryHash = function (account1, account2, currency) {
currency = Currency.from_json(currency);
account1 = UInt160.from_json(account1);
account2 = UInt160.from_json(account2);
if (!account1.is_valid()) {
throw new Error("Invalid first account");
}
if (!account2.is_valid()) {
throw new Error("Invalid second account");
}
if (!currency.is_valid()) {
throw new Error("Invalid currency");
}
// The lower ID has to come first
if (account1.to_bn().greaterEquals(account2.to_bn())) {
var tmp = account2;
account2 = account1;
account1 = tmp;
}
var index = new SerializedObject();
index.append([0, Ledger.space.rippleState.charCodeAt(0)]);
index.append(account1.to_bytes());
index.append(account2.to_bytes());
index.append(currency.to_bytes());
return index.hash();
};
Ledger.prototype.parse_json = function (v) {
this.ledger_json = v;
};
@@ -37,4 +127,41 @@ Ledger.prototype.calc_tx_hash = function () {
return tx_map.hash();
};
/**
* @param options.sanity_test {Boolean}
*
* If `true`, will serialize each accountState item to binary and then back to
* json before finally serializing for hashing. This is mostly to expose any
* issues with ripple-lib's binary <--> json codecs.
*
*/
Ledger.prototype.calc_account_hash = function (options) {
var account_map = new SHAMap();
var erred;
this.ledger_json.accountState.forEach(function (le) {
var data = SerializedObject.from_json(le);
if (options != null && options.sanity_test) {
try {
var json = data.to_json();
data = SerializedObject.from_json(json);
} catch (e) {
console.log("account state item: ", le);
console.log("to_json() ",json);
console.log("exception: ", e);
erred = true;
}
};
account_map.add_item(le.index, data, SHAMapTreeNode.TYPE_ACCOUNT_STATE);
});
if (erred) {
throw new Error("There were errors with sanity_test"); // all logged above
}
return account_map.hash();
};
exports.Ledger = Ledger;

View File

@@ -0,0 +1,22 @@
/**
* Ripple ledger namespace prefixes.
*
* The Ripple ledger is a key-value store. In order to avoid name collisions,
* names are partitioned into namespaces.
*
* Each namespace is just a single character prefix.
*/
module.exports = {
account : 'a',
dirNode : 'd',
generatorMap : 'g',
nickname : 'n',
rippleState : 'r',
offer : 'o', // Entry for an offer.
ownerDir : 'O', // Directory of things owned by an account.
bookDir : 'B', // Directory of order books.
contract : 'c',
skipList : 's',
amendment : 'f',
feeSettings : 'e'
};

View File

@@ -1,13 +1,13 @@
/**
* Logging functionality for ripple-lib and any applications built on it.
*/
var Log = function (namespace) {
function Log(namespace) {
if (!namespace) {
this._namespace = [];
} else if (Array.isArray(namespace)) {
this._namespace = namespace;
} else {
this._namespace = [""+namespace];
this._namespace = [''+namespace];
}
this._prefix = this._namespace.concat(['']).join(': ');
@@ -23,25 +23,29 @@ var Log = function (namespace) {
* var log = require('ripple').log.sub('server');
*
* log.info('connection successful');
* // prints: "server: connection successful"
* // prints: 'server: connection successful'
*/
Log.prototype.sub = function (namespace) {
Log.prototype.sub = function(namespace) {
var subNamespace = this._namespace.slice();
if (namespace && "string" === typeof namespace) subNamespace.push(namespace);
if (namespace && typeof namespace === 'string') {
subNamespace.push(namespace);
}
var subLogger = new Log(subNamespace);
subLogger._setParent(this);
return subLogger;
};
Log.prototype._setParent = function (parentLogger) {
Log.prototype._setParent = function(parentLogger) {
this._parent = parentLogger;
};
Log.makeLevel = function (level) {
return function () {
arguments[0] = this._prefix + arguments[0];
Log.engine.logObject.apply(Log, Array.prototype.slice.call(arguments));
Log.makeLevel = function(level) {
return function() {
var args = Array.prototype.slice.call(arguments);
args[0] = this._prefix + args[0];
Log.engine.logObject.apply(Log, args);
};
};
@@ -53,7 +57,7 @@ Log.prototype.error = Log.makeLevel(4);
/**
* Basic logging connector.
*
* This engine has no formatting and works with the most basic of "console.log"
* This engine has no formatting and works with the most basic of 'console.log'
* implementations. This is the logging engine used in Node.js.
*/
var BasicLogEngine = {
@@ -77,12 +81,14 @@ var BasicLogEngine = {
* available.
*/
var NullLogEngine = {
logObject: function () {}
logObject: function() {}
};
Log.engine = NullLogEngine;
if (console && console.log) Log.engine = BasicLogEngine;
if (console && console.log) {
Log.engine = BasicLogEngine;
}
/**
* Provide a root logger as our main export.

203
src/js/ripple/message.js Normal file
View File

@@ -0,0 +1,203 @@
var async = require('async');
var crypto = require('crypto');
var sjcl = require('./utils').sjcl;
var Remote = require('./remote').Remote;
var Seed = require('./seed').Seed;
var KeyPair = require('./keypair').KeyPair;
var Account = require('./account').Account;
var UInt160 = require('./uint160').UInt160;
// Message class (static)
var Message = {};
Message.HASH_FUNCTION = sjcl.hash.sha512.hash;
Message.MAGIC_BYTES = 'Ripple Signed Message:\n';
var REGEX_HEX = /^[0-9a-fA-F]+$/;
var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/;
/**
* Produce a Base64-encoded signature on the given message with
* the string 'Ripple Signed Message:\n' prepended.
*
* Note that this signature uses the signing function that includes
* a recovery_factor to be able to extract the public key from the signature
* without having to pass the public key along with the signature.
*
* @static
*
* @param {String} message
* @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key
* @param {RippleAddress} [The first key] account Field to specify the signing account.
* If this is omitted the first account produced by the secret generator will be used.
* @returns {Base64-encoded String} signature
*/
Message.signMessage = function(message, secret_key, account) {
return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key, account);
};
/**
* Produce a Base64-encoded signature on the given hex-encoded hash.
*
* Note that this signature uses the signing function that includes
* a recovery_factor to be able to extract the public key from the signature
* without having to pass the public key along with the signature.
*
* @static
*
* @param {bitArray|Hex-encoded String} hash
* @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key
* @param {RippleAddress} [The first key] account Field to specify the signing account.
* If this is omitted the first account produced by the secret generator will be used.
* @returns {Base64-encoded String} signature
*/
Message.signHash = function(hash, secret_key, account) {
if (typeof hash === 'string' && /^[0-9a-fA-F]+$/.test(hash)) {
hash = sjcl.codec.hex.toBits(hash);
}
if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') {
throw new Error('Hash must be a bitArray or hex-encoded string');
}
if (!(secret_key instanceof sjcl.ecc.ecdsa.secretKey)) {
secret_key = Seed.from_json(secret_key).get_key(account)._secret;
}
var signature_bits = secret_key.signWithRecoverablePublicKey(hash);
var signature_base64 = sjcl.codec.base64.fromBits(signature_bits);
return signature_base64;
};
/**
* Verify the signature on a given message.
*
* Note that this function is asynchronous.
* The ripple-lib remote is used to check that the public
* key extracted from the signature corresponds to one that is currently
* active for the given account.
*
* @static
*
* @param {String} data.message
* @param {RippleAddress} data.account
* @param {Base64-encoded String} data.signature
* @param {ripple-lib Remote} remote
* @param {Function} callback
*
* @callback callback
* @param {Error} error
* @param {boolean} is_valid true if the signature is valid, false otherwise
*/
Message.verifyMessageSignature = function(data, remote, callback) {
if (typeof data.message === 'string') {
data.hash = Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message);
} else {
return callback(new Error('Data object must contain message field to verify signature'));
}
return Message.verifyHashSignature(data, remote, callback);
};
/**
* Verify the signature on a given hash.
*
* Note that this function is asynchronous.
* The ripple-lib remote is used to check that the public
* key extracted from the signature corresponds to one that is currently
* active for the given account.
*
* @static
*
* @param {bitArray|Hex-encoded String} data.hash
* @param {RippleAddress} data.account
* @param {Base64-encoded String} data.signature
* @param {ripple-lib Remote} remote
* @param {Function} callback
*
* @callback callback
* @param {Error} error
* @param {boolean} is_valid true if the signature is valid, false otherwise
*/
Message.verifyHashSignature = function(data, remote, callback) {
var hash,
account,
signature;
if(typeof callback !== 'function') {
throw new Error('Must supply callback function');
}
hash = data.hash;
if (hash && typeof hash === 'string' && REGEX_HEX.test(hash)) {
hash = sjcl.codec.hex.toBits(hash);
}
if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') {
return callback(new Error('Hash must be a bitArray or hex-encoded string'));
}
account = data.account || data.address;
if (!account || !UInt160.from_json(account).is_valid()) {
return callback(new Error('Account must be a valid ripple address'));
}
signature = data.signature;
if (typeof signature !== 'string' || !REGEX_BASE64.test(signature)) {
return callback(new Error('Signature must be a Base64-encoded string'));
}
signature = sjcl.codec.base64.toBits(signature);
if (!(remote instanceof Remote) || remote.state !== 'online') {
return callback(new Error('Must supply connected Remote to verify signature'));
}
function recoverPublicKey (async_callback) {
var public_key;
try {
public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
} catch (err) {
return async_callback(err);
}
if (public_key) {
async_callback(null, public_key);
} else {
async_callback(new Error('Could not recover public key from signature'));
}
};
function checkPublicKeyIsValid (public_key, async_callback) {
// Get hex-encoded public key
var key_pair = new KeyPair();
key_pair._pubkey = public_key;
var public_key_hex = key_pair.to_hex_pub();
var account_class_instance = new Account(remote, account);
account_class_instance.publicKeyIsActive(public_key_hex, async_callback);
};
var steps = [
recoverPublicKey,
checkPublicKeyIsValid
];
async.waterfall(steps, callback);
};
exports.Message = Message;

View File

@@ -15,7 +15,7 @@ function Meta(raw_data) {
raw_data.AffectedNodes.forEach(function(an) {
var result = { };
if (result.diffType = self.diffType(an)) {
if ((result.diffType = self.diffType(an))) {
an = an[result.diffType];
result.entryType = an.LedgerEntryType;
@@ -88,7 +88,7 @@ Meta.prototype.diffType = function(an) {
* The second parameter to the callback is the index of the node in the metadata
* (first entry is index 0).
*/
Meta.prototype.each = function (fn) {
Meta.prototype.each = function(fn) {
for (var i = 0, l = this.nodes.length; i < l; i++) {
fn(this.nodes[i], i);
}
@@ -103,7 +103,7 @@ Meta.prototype.each = function (fn) {
]).forEach(function(fn) {
Meta.prototype[fn] = function() {
return Array.prototype[fn].apply(this.nodes, arguments);
}
};
});
var amountFieldsAffectingIssuer = [
@@ -113,12 +113,12 @@ var amountFieldsAffectingIssuer = [
'TakerGets'
];
Meta.prototype.getAffectedAccounts = function () {
Meta.prototype.getAffectedAccounts = function() {
var accounts = [ ];
// This code should match the behavior of the C++ method:
// TransactionMetaSet::getAffectedAccounts
this.nodes.forEach(function (an) {
this.nodes.forEach(function(an) {
var fields = (an.diffType === 'CreatedNode') ? an.fieldsNew : an.fieldsFinal;
for (var i in fields) {
var field = fields[i];
@@ -137,20 +137,26 @@ Meta.prototype.getAffectedAccounts = function () {
return utils.arrayUnique(accounts);
};
Meta.prototype.getAffectedBooks = function () {
Meta.prototype.getAffectedBooks = function() {
var books = [ ];
this.nodes.forEach(function (an) {
if (an.entryType !== 'Offer') return;
this.nodes.forEach(function(an) {
if (an.entryType !== 'Offer') {
return;
}
var gets = Amount.from_json(an.fields.TakerGets);
var pays = Amount.from_json(an.fields.TakerPays);
var getsKey = gets.currency().to_json();
if (getsKey !== 'XRP') getsKey += '/' + gets.issuer().to_json();
if (getsKey !== 'XRP') {
getsKey += '/' + gets.issuer().to_json();
}
var paysKey = pays.currency().to_json();
if (paysKey !== 'XRP') paysKey += '/' + pays.issuer().to_json();
if (paysKey !== 'XRP') {
paysKey += '/' + pays.issuer().to_json();
}
var key = [ getsKey, paysKey ].join(':');

View File

@@ -8,8 +8,8 @@
// var network = require("./network.js");
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var extend = require('extend');
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
@@ -18,7 +18,7 @@ var Currency = require('./currency').Currency;
function OrderBook(remote, currency_gets, issuer_gets, currency_pays, issuer_pays, key) {
EventEmitter.call(this);
var self = this;
var self = this;
this._remote = remote;
this._currency_gets = currency_gets;
@@ -30,15 +30,15 @@ function OrderBook(remote, currency_gets, issuer_gets, currency_pays, issuer_pay
// We consider ourselves synchronized if we have a current copy of the offers,
// we are online and subscribed to updates.
this._sync = false;
this._sync = false;
// Offers
this._offers = [ ];
this._offers = [ ];
function listenerAdded(type, listener) {
if (~OrderBook.subscribe_events.indexOf(type)) {
self._subs += 1;
if (self._subs == 1 && self._remote._connected) {
if (self._subs === 1 && self._remote._connected) {
self._subscribe();
}
}
@@ -140,11 +140,11 @@ OrderBook.prototype.to_json = function () {
};
if (this._currency_gets !== 'XRP') {
json['taker_gets']['issuer'] = this._issuer_gets;
json.taker_gets.issuer = this._issuer_gets;
}
if (this._currency_pays !== 'XRP') {
json['taker_pays']['issuer'] = this._issuer_pays;
json.taker_pays.issuer = this._issuer_pays;
}
return json;
@@ -188,7 +188,9 @@ OrderBook.prototype.notify = function (message) {
var trade_pays = this.trade('pays');
function handleTransaction(an) {
if (an.entryType !== 'Offer' || an.bookKey !== self._key) return;
if (an.entryType !== 'Offer' || an.bookKey !== self._key) {
return;
}
var i, l, offer;
@@ -197,8 +199,9 @@ OrderBook.prototype.notify = function (message) {
case 'ModifiedNode':
var deletedNode = an.diffType === 'DeletedNode';
for (i = 0, l = self._offers.length; i < l; i++) {
for (i=0, l=self._offers.length; i<l; i++) {
offer = self._offers[i];
if (offer.index === an.ledgerIndex) {
if (deletedNode) {
self._offers.splice(i, 1);
@@ -207,13 +210,16 @@ OrderBook.prototype.notify = function (message) {
// safe assumption, but should be checked.
extend(offer, an.fieldsFinal);
}
changed = true;
break;
}
}
// We don't want to count a OfferCancel as a trade
if (message.transaction.TransactionType === 'OfferCancel') return;
if (message.transaction.TransactionType === 'OfferCancel') {
return;
}
trade_gets = trade_gets.add(an.fieldsPrev.TakerGets);
trade_pays = trade_pays.add(an.fieldsPrev.TakerPays);
@@ -225,11 +231,12 @@ OrderBook.prototype.notify = function (message) {
break;
case 'CreatedNode':
var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets);
// XXX Should use Amount#from_quality
var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets, {reference_date: new Date()});
for (i = 0, l = self._offers.length; i < l; i++) {
offer = self._offers[i];
var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets);
var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets, {reference_date: new Date()});
if (price.compareTo(priceItem) <= 0) {
var obj = an.fields;
@@ -271,6 +278,7 @@ OrderBook.prototype.notifyTx = OrderBook.prototype.notify;
*/
OrderBook.prototype.offers = function (callback) {
var self = this;
if (typeof callback === 'function') {
if (this._sync) {
callback(this._offers);
@@ -278,6 +286,7 @@ OrderBook.prototype.offers = function (callback) {
this.once('model', callback);
}
}
return this;
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,8 @@ var EventEmitter = require('events').EventEmitter;
var util = require('util');
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var Transaction = require('./transaction').Transaction;
var Account = require('./account').Account;
var Meta = require('./meta').Meta;
var OrderBook = require('./orderbook').OrderBook;
var RippleError = require('./rippleerror').RippleError;
var Server = require('./server').Server;
// Request events emitted:
// 'success' : Request successful.
@@ -17,11 +14,12 @@ var RippleError = require('./rippleerror').RippleError;
function Request(remote, command) {
EventEmitter.call(this);
this.remote = remote;
this.requested = false;
this.message = {
command : command,
id : void(0)
this.remote = remote;
this.requested = false;
this.message = {
command: command,
id: void(0)
};
};
@@ -34,17 +32,20 @@ Request.prototype.broadcast = function() {
// Send the request to a remote.
Request.prototype.request = function(remote) {
if (this.requested) return;
if (this.requested) {
return;
}
this.requested = true;
this.on('error', new Function);
this.on('error', function(){});
this.emit('request', remote);
if (this._broadcast) {
this.remote._servers.forEach(function(server) {
this.setServer(server);
this.remote.request(this);
}, this );
}, this);
} else {
this.remote.request(this);
}
@@ -53,24 +54,36 @@ Request.prototype.request = function(remote) {
};
Request.prototype.callback = function(callback, successEvent, errorEvent) {
if (callback && typeof callback === 'function') {
var self = this;
var self = this;
function request_success(message) {
if (this.requested || typeof callback !== 'function') {
return this;
}
var called = false;
function requestSuccess(message) {
if (!called) {
called = true;
callback.call(self, null, message);
}
};
function requestError(error) {
if (!called) {
called = true;
function request_error(error) {
if (!(error instanceof RippleError)) {
error = new RippleError(error);
}
callback.call(self, error);
}
};
this.once(successEvent || 'success', request_success);
this.once(errorEvent || 'error' , request_error);
this.request();
}
this.once(successEvent || 'success', requestSuccess);
this.once(errorEvent || 'error' , requestError);
this.request();
return this;
};
@@ -78,12 +91,13 @@ Request.prototype.callback = function(callback, successEvent, errorEvent) {
Request.prototype.timeout = function(duration, callback) {
var self = this;
function requested() {
self.timeout(duration, callback);
};
if (!this.requested) {
function requested() {
self.timeout(duration, callback);
}
this.once('request', requested);
return;
// Defer until requested
return this.once('request', requested);
}
var emit = this.emit;
@@ -91,7 +105,11 @@ Request.prototype.timeout = function(duration, callback) {
var timeout = setTimeout(function() {
timed_out = true;
if (typeof callback === 'function') callback();
if (typeof callback === 'function') {
callback();
}
emit.call(self, 'timeout');
}, duration);
@@ -112,10 +130,13 @@ Request.prototype.setServer = function(server) {
case 'object':
selected = server;
break;
case 'string':
// Find server by URL
var servers = this.remote._servers;
for (var i=0, s; s=servers[i]; i++) {
if (s._host === server) {
for (var i=0, s; (s=servers[i]); i++) {
if (s._url === server) {
selected = s;
break;
}
@@ -123,18 +144,19 @@ Request.prototype.setServer = function(server) {
break;
};
this.server = selected;
if (selected instanceof Server) {
this.server = selected;
}
return this;
};
Request.prototype.buildPath = function(build) {
if (this.remote.local_signing) {
throw new Error(
'`build_path` is completely ignored when doing local signing as ' +
'`Paths` is a component of the signed blob. The `tx_blob` is signed,' +
'sealed and delivered, and the txn unmodified after' );
'`build_path` is completely ignored when doing local signing as '
+ '`Paths` is a component of the signed blob. The `tx_blob` is signed,'
+ 'sealed and delivered, and the txn unmodified after' );
}
if (build) {
@@ -142,8 +164,9 @@ Request.prototype.buildPath = function(build) {
} else {
// ND: rippled currently intreprets the mere presence of `build_path` as the
// value being `truthy`
delete this.message.build_path
delete this.message.build_path;
}
return this;
};
@@ -153,6 +176,7 @@ Request.prototype.ledgerChoose = function(current) {
} else {
this.message.ledger_hash = this.remote._ledger_hash;
}
return this;
};
@@ -171,19 +195,19 @@ Request.prototype.ledgerIndex = function(ledger_index) {
return this;
};
Request.prototype.ledgerSelect = function(ledger_spec) {
switch (ledger_spec) {
Request.prototype.ledgerSelect = function(ledger) {
switch (ledger) {
case 'current':
case 'closed':
case 'verified':
this.message.ledger_index = ledger_spec;
case 'validated':
this.message.ledger_index = ledger;
break;
default:
if (Number(ledger_spec)) {
this.message.ledger_index = ledger_spec;
} else {
this.message.ledger_hash = ledger_spec;
if (isNaN(ledger)) {
this.message.ledger_hash = ledger;
} else if ((ledger = Number(ledger))) {
this.message.ledger_index = ledger;
}
break;
}
@@ -196,8 +220,8 @@ Request.prototype.accountRoot = function(account) {
return this;
};
Request.prototype.index = function(hash) {
this.message.index = hash;
Request.prototype.index = function(index) {
this.message.index = index;
return this;
};
@@ -214,34 +238,34 @@ Request.prototype.offerId = function(account, sequence) {
// --> index : ledger entry index.
Request.prototype.offerIndex = function(index) {
this.message.offer = index;
this.message.offer = index;
return this;
};
Request.prototype.secret = function(secret) {
if (secret) {
this.message.secret = secret;
this.message.secret = secret;
}
return this;
};
Request.prototype.txHash = function(hash) {
this.message.tx_hash = hash;
this.message.tx_hash = hash;
return this;
};
Request.prototype.txJson = function(json) {
this.message.tx_json = json;
this.message.tx_json = json;
return this;
};
Request.prototype.txBlob = function(json) {
this.message.tx_blob = json;
this.message.tx_blob = json;
return this;
};
Request.prototype.rippleState = function(account, issuer, currency) {
this.message.ripple_state = {
this.message.ripple_state = {
currency : currency,
accounts : [
UInt160.json_rewrite(account),
@@ -295,7 +319,7 @@ Request.prototype.books = function(books, snapshot) {
// Reset list of books (this method overwrites the current list)
this.message.books = [ ];
for (var i = 0, l = books.length; i < l; i++) {
for (var i=0, l=books.length; i<l; i++) {
var book = books[i];
this.addBook(book, snapshot);
}
@@ -303,9 +327,9 @@ Request.prototype.books = function(books, snapshot) {
return this;
};
Request.prototype.addBook = function (book, snapshot) {
Request.prototype.addBook = function(book, snapshot) {
if (!Array.isArray(this.message.books)) {
this.message.books = [];
this.message.books = [ ];
}
var json = { };

View File

@@ -6,8 +6,9 @@ function RippleError(code, message) {
case 'object':
extend(this, code);
break;
case 'string':
this.result = code;
this.result = code;
this.result_message = message;
break;
}
@@ -17,11 +18,13 @@ function RippleError(code, message) {
this.result_message = this.message = (this.result_message);
var stack;
if (!!Error.captureStackTrace)
if (!!Error.captureStackTrace) {
Error.captureStackTrace(this, code || this);
else if (stack = new Error().stack)
} else if ((stack = new Error().stack)) {
this.stack = stack;
}
}
};
util.inherits(RippleError, Error);

133
src/js/ripple/rippletxt.js Normal file
View File

@@ -0,0 +1,133 @@
var request = require('superagent');
var Currency = require('./currency').Currency;
var RippleTxt = {
txts : { }
};
RippleTxt.urlTemplates = [
'https://{{domain}}/ripple.txt',
'https://www.{{domain}}/ripple.txt',
'https://ripple.{{domain}}/ripple.txt',
'http://{{domain}}/ripple.txt',
'http://www.{{domain}}/ripple.txt',
'http://ripple.{{domain}}/ripple.txt'
];
/**
* Gets the ripple.txt file for the given domain
* @param {string} domain - Domain to retrieve file from
* @param {function} fn - Callback function
*/
RippleTxt.get = function(domain, fn) {
var self = this;
if (self.txts[domain]) {
return fn(null, self.txts[domain]);
}
;(function nextUrl(i) {
var url = RippleTxt.urlTemplates[i];
if (!url) {
return fn(new Error('No ripple.txt found'));
}
url = url.replace('{{domain}}', domain);
request.get(url, function(err, resp) {
if (err || !resp.text) {
return nextUrl(++i);
}
var sections = self.parse(resp.text);
self.txts[domain] = sections;
fn(null, sections);
});
})(0);
};
/**
* Parse a ripple.txt file
* @param {string} txt - Unparsed ripple.txt data
*/
RippleTxt.parse = function(txt) {
var currentSection = '';
var sections = { };
txt = txt.replace(/\r?\n/g, '\n').split('\n');
for (var i = 0, l = txt.length; i < l; i++) {
var line = txt[i];
if (!line.length || line[0] === '#') {
continue;
}
if (line[0] === '[' && line[line.length - 1] === ']') {
currentSection = line.slice(1, line.length - 1);
sections[currentSection] = [];
} else {
line = line.replace(/^\s+|\s+$/g, '');
if (sections[currentSection]) {
sections[currentSection].push(line);
}
}
}
return sections;
};
/**
* extractDomain
* attempt to extract the domain from a given url
* returns the url if unsuccessful
* @param {Object} url
*/
RippleTxt.extractDomain = function (url) {
match = /[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?([^.\?][^\?.]+?)?$/.exec(url);
return match && match[0] ? match[0] : url;
};
/**
* getCurrencies
* returns domain, issuer account and currency object
* for each currency found in the domain's ripple.txt file
* @param {Object} domain
* @param {Object} fn
*/
RippleTxt.getCurrencies = function(domain, fn) {
domain = RippleTxt.extractDomain(domain);
this.get(domain, function(err, txt) {
if (err) {
return fn(err);
}
if (err || !txt.currencies || !txt.accounts) {
return fn(null, []);
}
//NOTE: this won't be accurate if there are
//multiple issuer accounts with different
//currencies associated with each.
var issuer = txt.accounts[0];
var currencies = [];
txt.currencies.forEach(function(currency) {
currencies.push({
issuer : issuer,
currency : Currency.from_json(currency),
domain : domain
});
});
fn(null, currencies);
});
};
exports.RippleTxt = RippleTxt;

View File

@@ -2,20 +2,21 @@
// Seed support
//
var utils = require('./utils');
var sjcl = utils.sjcl;
var extend = require('extend');
var extend = require('extend');
var utils = require('./utils');
var sjcl = utils.sjcl;
var BigInteger = utils.jsbn.BigInteger;
var Base = require('./base').Base;
var UInt = require('./uint').UInt;
var UInt256 = require('./uint256').UInt256;
var UInt160 = require('./uint160').UInt160;
var KeyPair = require('./keypair').KeyPair;
var Seed = extend(function () {
// Internal form: NaN or BigInteger
this._curve = sjcl.ecc.curves['c256'];
this._curve = sjcl.ecc.curves.c256;
this._value = NaN;
}, UInt);
@@ -30,7 +31,7 @@ Seed.prototype.parse_json = function (j) {
if (!j.length) {
this._value = NaN;
// XXX Should actually always try and continue if it failed.
} else if (j[0] === "s") {
} else if (j[0] === 's') {
this._value = Base.decode_check(Base.VER_FAMILY_SEED, j);
} else if (j.length === 32) {
this._value = this.parse_hex(j);
@@ -47,7 +48,7 @@ Seed.prototype.parse_json = function (j) {
Seed.prototype.parse_passphrase = function (j) {
if (typeof j !== 'string') {
throw new Error("Passphrase must be a string");
throw new Error('Passphrase must be a string');
}
var hash = sjcl.hash.sha512.hash(sjcl.codec.utf8String.toBits(j));
@@ -83,15 +84,33 @@ function SHA256_RIPEMD160(bits) {
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
};
Seed.prototype.get_key = function (account_id) {
/**
* @param account
* {undefined} take first, default, KeyPair
*
* {Number} specifies the account number of the KeyPair
* desired.
*
* {Uint160} (from_json able), specifies the address matching the KeyPair
* that is desired.
*/
Seed.prototype.get_key = function (account) {
var account_number = 0, address;
if (!this.is_valid()) {
throw new Error("Cannot generate keys from invalid seed!");
throw new Error('Cannot generate keys from invalid seed!');
}
if (account) {
if (typeof account === 'number') {
account_number = account;
} else {
address = UInt160.from_json(account);
}
}
// XXX Should loop over keys until we find the right one
var private_gen, public_gen;
var curve = this._curve;
var seq = 0, i = 0;
var i = 0;
do {
private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.to_bytes(), i)));
@@ -101,16 +120,30 @@ Seed.prototype.get_key = function (account_id) {
public_gen = curve.G.mult(private_gen);
var sec;
i = 0;
var key_pair;
var max_loops = 1000; // TODO
do {
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i)));
i++;
} while (!curve.r.greaterEquals(sec));
i = 0;
sec = sec.add(private_gen).mod(curve.r);
do {
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), account_number), i)));
i++;
} while (!curve.r.greaterEquals(sec));
return KeyPair.from_bn_secret(sec);
account_number++;
sec = sec.add(private_gen).mod(curve.r);
key_pair = KeyPair.from_bn_secret(sec);
if (--max_loops <= 0) {
// We are almost certainly looking for an account that would take same
// value of $too_long {forever, ...}
throw new Error('Too many loops looking for KeyPair yielding '+
address.to_json() +' from ' + this.to_json());
};
} while (address && !key_pair.get_address().equals(address));
return key_pair;
};
exports.Seed = Seed;

View File

@@ -1,11 +1,12 @@
var binformat = require('./binformat');
var extend = require('extend');
var stypes = require('./serializedtypes');
var UInt256 = require('./uint256').UInt256;
var assert = require('assert');
var assert = require('assert');
var extend = require('extend');
var binformat = require('./binformat');
var stypes = require('./serializedtypes');
var UInt256 = require('./uint256').UInt256;
var Crypt = require('./crypt').Crypt;
var utils = require('./utils');
var utils = require('./utils');
var sjcl = utils.sjcl;
var sjcl = utils.sjcl;
var BigInteger = utils.jsbn.BigInteger;
var TRANSACTION_TYPES = { };
@@ -39,33 +40,46 @@ function SerializedObject(buf) {
this.pointer = 0;
};
SerializedObject.from_json = function (obj) {
SerializedObject.from_json = function(obj) {
// Create a copy of the object so we don't modify it
var obj = extend({}, obj);
var so = new SerializedObject;
var so = new SerializedObject();
var typedef;
if ("number" === typeof obj.TransactionType) {
if (typeof obj.TransactionType === 'number') {
obj.TransactionType = SerializedObject.lookup_type_tx(obj.TransactionType);
if (!obj.TransactionType) {
throw new Error('Transaction type ID is invalid.');
}
}
if ("string" === typeof obj.TransactionType) {
typedef = binformat.tx[obj.TransactionType];
if (typeof obj.LedgerEntryType === 'number') {
obj.LedgerEntryType = SerializedObject.lookup_type_le(obj.LedgerEntryType);
if (!obj.LedgerEntryType) {
throw new Error('LedgerEntryType ID is invalid.');
}
}
if (typeof obj.TransactionType === 'string') {
typedef = binformat.tx[obj.TransactionType];
if (!Array.isArray(typedef)) {
throw new Error('Transaction type is invalid');
}
typedef = typedef.slice();
obj.TransactionType = typedef.shift();
} else if ("undefined" !== typeof obj.LedgerEntryType) {
// XXX: TODO
throw new Error('Ledger entry binary format not yet implemented.');
} else if ("object" === typeof obj.AffectedNodes) {
} else if (typeof obj.LedgerEntryType === 'string') {
typedef = binformat.ledger[obj.LedgerEntryType];
if (!Array.isArray(typedef)) {
throw new Error('LedgerEntryType is invalid');
}
typedef = typedef.slice();
obj.LedgerEntryType = typedef.shift();
} else if (typeof obj.AffectedNodes === 'object') {
typedef = binformat.metadata;
} else {
throw new Error('Object to be serialized must contain either' +
@@ -79,49 +93,52 @@ SerializedObject.from_json = function (obj) {
return so;
};
SerializedObject.check_no_missing_fields = function (typedef, obj) {
SerializedObject.check_no_missing_fields = function(typedef, obj) {
var missing_fields = [];
for (var i = typedef.length - 1; i >= 0; i--) {
var spec = typedef[i];
var field = spec[0]
var field = spec[0];
var requirement = spec[1];
if (binformat.REQUIRED === requirement && obj[field] == null) {
if (binformat.REQUIRED === requirement && obj[field] === void(0)) {
missing_fields.push(field);
};
};
if (missing_fields.length > 0) {
var object_name;
if (obj.TransactionType != null) {
if (obj.TransactionType !== void(0)) {
object_name = SerializedObject.lookup_type_tx(obj.TransactionType);
} else if (obj.LedgerEntryType != null){
object_name = SerializedObject.lookup_type_le(obj.LedgerEntryType);
} else {
object_name = "TransactionMetaData";
} /*else {
TODO: LedgerEntryType ...
}*/
throw new Error(object_name + " is missing fields: " +
}
throw new Error(object_name + " is missing fields: " +
JSON.stringify(missing_fields));
};
}
};
SerializedObject.prototype.append = function (bytes) {
SerializedObject.prototype.append = function(bytes) {
if (bytes instanceof SerializedObject) {
bytes = bytes.buffer;
}
this.buffer = this.buffer.concat(bytes);
this.pointer += bytes.length;
};
SerializedObject.prototype.resetPointer = function () {
SerializedObject.prototype.resetPointer = function() {
this.pointer = 0;
};
function readOrPeek(advance) {
return function(bytes) {
var start = this.pointer;
var end = start + bytes;
var end = start + bytes;
if (end > this.buffer.length) {
throw new Error('Buffer length exceeded');
@@ -134,18 +151,18 @@ function readOrPeek(advance) {
}
return result;
}
};
};
SerializedObject.prototype.read = readOrPeek(true);
SerializedObject.prototype.peek = readOrPeek(false);
SerializedObject.prototype.to_bits = function () {
SerializedObject.prototype.to_bits = function() {
return sjcl.codec.bytes.toBits(this.buffer);
};
SerializedObject.prototype.to_hex = function () {
SerializedObject.prototype.to_hex = function() {
return sjcl.codec.hex.fromBits(this.to_bits()).toUpperCase();
};
@@ -164,7 +181,7 @@ SerializedObject.prototype.to_json = function() {
this.pointer = old_pointer;
return output;
}
};
SerializedObject.jsonify_structure = function(structure, field_name) {
var output;
@@ -186,14 +203,20 @@ SerializedObject.jsonify_structure = function(structure, field_name) {
}
break;
case 'object':
if (!structure) break; //null
if (structure === null) {
break;
}
if (typeof structure.to_json === 'function') {
output = structure.to_json();
} else if (structure instanceof BigInteger) {
output = structure.toString(16).toUpperCase();
} else {
output = new structure.constructor; //new Array or Object
//new Array or Object
output = new structure.constructor();
var keys = Object.keys(structure);
for (var i=0, l=keys.length; i<l; i++) {
var key = keys[i];
output[key] = SerializedObject.jsonify_structure(structure[key], key);
@@ -207,7 +230,7 @@ SerializedObject.jsonify_structure = function(structure, field_name) {
return output;
};
SerializedObject.prototype.serialize = function (typedef, obj) {
SerializedObject.prototype.serialize = function(typedef, obj) {
// Serialize object without end marker
stypes.Object.serialize(this, obj, true);
@@ -223,23 +246,26 @@ SerializedObject.prototype.serialize = function (typedef, obj) {
*/
};
SerializedObject.prototype.hash = function (prefix) {
SerializedObject.prototype.hash = function(prefix) {
var sign_buffer = new SerializedObject();
stypes.Int32.serialize(sign_buffer, prefix);
// Add hashing prefix
if ("undefined" !== typeof prefix) {
stypes.Int32.serialize(sign_buffer, prefix);
}
// Copy buffer to temporary buffer
sign_buffer.append(this.buffer);
return sign_buffer.hash_sha512_half();
// XXX We need a proper Buffer class then Crypt could accept that
var bits = sjcl.codec.bytes.toBits(sign_buffer.buffer);
return Crypt.hashSha512Half(bits);
};
// DEPRECATED
SerializedObject.prototype.signing_hash = SerializedObject.prototype.hash;
SerializedObject.prototype.hash_sha512_half = function () {
var bits = sjcl.codec.bytes.toBits(this.buffer);
var hash = sjcl.bitArray.bitSlice(sjcl.hash.sha512.hash(bits), 0, 256);
return UInt256.from_hex(sjcl.codec.hex.fromBits(hash));
};
SerializedObject.prototype.serialize_field = function (spec, obj) {
SerializedObject.prototype.serialize_field = function(spec, obj) {
var name = spec[0];
var presence = spec[1];
var field_id = spec[2];
@@ -262,7 +288,7 @@ SerializedObject.prototype.serialize_field = function (spec, obj) {
}
};
SerializedObject.get_field_header = function (type_id, field_id) {
SerializedObject.get_field_header = function(type_id, field_id) {
var buffer = [ 0 ];
if (type_id > 0xF) {
@@ -280,7 +306,7 @@ SerializedObject.get_field_header = function (type_id, field_id) {
return buffer;
};
SerializedObject.sort_typedef = function (typedef) {
SerializedObject.sort_typedef = function(typedef) {
assert(Array.isArray(typedef));
function sort_field_compare(a, b) {
@@ -291,9 +317,14 @@ SerializedObject.sort_typedef = function (typedef) {
return typedef.sort(sort_field_compare);
};
SerializedObject.lookup_type_tx = function (id) {
assert(typeof id === 'number');
SerializedObject.lookup_type_tx = function(id) {
assert.strictEqual(typeof id, 'number');
return TRANSACTION_TYPES[id];
};
SerializedObject.lookup_type_le = function (id) {
assert(typeof id === 'number');
return LEDGER_ENTRY_TYPES[id];
};
exports.SerializedObject = SerializedObject;

View File

@@ -22,8 +22,8 @@ var Amount = amount.Amount;
var Currency = amount.Currency;
// Shortcuts
var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes;
var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes;
var BigInteger = utils.jsbn.BigInteger;
@@ -79,11 +79,7 @@ SerializedType.serialize_varint = function (so, val) {
so.append([193 + (val >>> 8), val & 0xff]);
} else if (val <= 918744) {
val -= 12481;
so.append([
241 + (val >>> 16),
val >>> 8 & 0xff,
val & 0xff
]);
so.append([ 241 + (val >>> 16), val >>> 8 & 0xff, val & 0xff ]);
} else {
throw new Error('Variable integer overflow.');
}
@@ -105,7 +101,7 @@ SerializedType.prototype.parse_varint = function (so) {
} else if (b1 <= 254) {
b2 = so.read(1)[0];
b3 = so.read(1)[0];
result = 12481 + (b1 - 241) * 65536 + b2 * 256 + b3
result = 12481 + (b1 - 241) * 65536 + b2 * 256 + b3;
}
return result;
@@ -142,7 +138,7 @@ function readAndSum(so, bytes) {
var sum = 0;
if (bytes > 4) {
throw new Error("This function only supports up to four bytes.");
throw new Error('This function only supports up to four bytes.');
}
for (var i=0; i<bytes; i++) {
@@ -224,7 +220,10 @@ var STInt64 = exports.Int64 = new SerializedType({
serialize_hex(so, hex, true); //noLength = true
},
parse: function (so) {
var result = new BigInteger(so.read(8), 256);
var bytes = so.read(8);
// We need to add a 0, so if the high bit is set it won't think it's a
// pessimistic numeric fraek. What doth lief?
var result = new BigInteger([0].concat(bytes), 256);
assert(result instanceof BigInteger);
return result;
}
@@ -328,7 +327,10 @@ var STAmount = exports.Amount = new SerializedType({
// Amount enforces the range correctly, but we'll clear them anyway just
// so this code can make certain guarantees about the encoded value.
valueBytes[0] &= 0x3f;
if (!amount.is_negative()) valueBytes[0] |= 0x40;
if (!amount.is_negative()) {
valueBytes[0] |= 0x40;
}
} else {
var hi = 0, lo = 0;
@@ -337,7 +339,10 @@ var STAmount = exports.Amount = new SerializedType({
if (!amount.is_zero()) {
// Second bit: non-negative?
if (!amount.is_negative()) hi |= 1 << 30;
if (!amount.is_negative()) {
hi |= 1 << 30;
}
// Next eight bits: offset/exponent
hi |= ((97 + amount._offset) & 0xff) << 22;
// Remaining 52 bits: mantissa
@@ -465,11 +470,18 @@ var STPathSet = exports.PathSet = new SerializedType({
//if (entry.hasOwnProperty('_value')) {entry = entry._value;}
var type = 0;
if (entry.account) type |= this.typeAccount;
if (entry.currency) type |= this.typeCurrency;
if (entry.issuer) type |= this.typeIssuer;
if (entry.account) {
type |= this.typeAccount;
}
if (entry.currency) {
type |= this.typeCurrency;
}
if (entry.issuer) {
type |= this.typeIssuer;
}
STInt8.serialize(so, type);
if (entry.account) {
so.append(UInt160.from_json(entry.account).to_bytes());
}
@@ -484,6 +496,7 @@ var STPathSet = exports.PathSet = new SerializedType({
}
}
}
STInt8.serialize(so, this.typeEnd);
},
parse: function (so) {
@@ -512,40 +525,40 @@ var STPathSet = exports.PathSet = new SerializedType({
if (current_path) { //close the current path, if there is one,
path_list.push(current_path);
}
current_path = []; //and start a new one.
current_path = [ ]; //and start a new one.
continue;
}
//It's an entry-begin tag.
//console.log('It's an entry-begin tag.');
var entry = {};
if (tag_byte & this.typeAccount) {
//console.log('entry.account');
/*var bta = so.read(20);
console.log('BTA:', bta);*/
entry.account = STHash160.parse(so);
entry.account.set_version(Base.VER_ACCOUNT_ID);
}
if (tag_byte & this.typeCurrency) {
//console.log('entry.currency');
entry.currency = STCurrency.parse(so);
if (entry.currency.to_json() === 'XRP' && !entry.currency.is_native()) {
entry.non_native = true;
}
}
if (tag_byte & this.typeIssuer) {
//console.log('entry.issuer');
entry.issuer = STHash160.parse(so);
// Enable and set correct type of base-58 encoding
entry.issuer.set_version(Base.VER_ACCOUNT_ID);
//console.log('DONE WITH ISSUER!');
}
if (entry.account || entry.currency || entry.issuer) {
current_path.push(entry);
} else {
//It's an entry-begin tag.
//console.log('It's an entry-begin tag.');
var entry = {};
if (tag_byte & this.typeAccount) {
//console.log('entry.account');
/*var bta = so.read(20);
console.log('BTA:', bta);*/
entry.account = STHash160.parse(so);
entry.account.set_version(Base.VER_ACCOUNT_ID);
}
if (tag_byte & this.typeCurrency) {
//console.log('entry.currency');
entry.currency = STCurrency.parse(so);
if (entry.currency.to_json() === "XRP" &&
!entry.currency.is_native()) {
entry.non_native = true;
}
}
if (tag_byte & this.typeIssuer) {
//console.log('entry.issuer');
entry.issuer = STHash160.parse(so);
// Enable and set correct type of base-58 encoding
entry.issuer.set_version(Base.VER_ACCOUNT_ID);
//console.log('DONE WITH ISSUER!');
}
if (entry.account || entry.currency || entry.issuer) {
current_path.push(entry);
} else {
throw new Error('Invalid path entry'); //It must have at least something in it.
}
throw new Error('Invalid path entry'); //It must have at least something in it.
}
}
@@ -591,11 +604,11 @@ function serialize(so, field_name, value) {
var field_bits = field_coordinates[1];
var tag_byte = (type_bits < 16 ? type_bits << 4 : 0) | (field_bits < 16 ? field_bits : 0);
if (field_name === "LedgerEntryType" && "string" === typeof value) {
if (field_name === 'LedgerEntryType' && 'string' === typeof value) {
value = binformat.ledger[value][0];
}
if (field_name === "TransactionResult" && "string" === typeof value) {
if (field_name === 'TransactionResult' && 'string' === typeof value) {
value = binformat.ter[value];
}
@@ -668,8 +681,8 @@ var STObject = exports.Object = new SerializedType({
});
keys.forEach(function (key) {
if ("undefined" === typeof binformat.fieldsInverseMap[key]) {
throw new Error("JSON contains unknown field: '" + key + "'");
if (typeof binformat.fieldsInverseMap[key] === 'undefined') {
throw new Error('JSON contains unknown field: "' + key + '"');
}
});
@@ -679,7 +692,11 @@ var STObject = exports.Object = new SerializedType({
for (var i=0; i<keys.length; i++) {
serialize(so, keys[i], val[keys[i]]);
}
if (!no_marker) STInt8.serialize(so, 0xe1); //Object ending marker
if (!no_marker) {
//Object ending marker
STInt8.serialize(so, 0xe1);
}
},
parse: function (so) {
@@ -708,7 +725,9 @@ var STArray = exports.Array = new SerializedType({
var value = val[i][field_name];
serialize(so, field_name, value);
}
STInt8.serialize(so, 0xf1); //Array ending marker
//Array ending marker
STInt8.serialize(so, 0xf1);
},
parse: function (so) {

View File

@@ -1,107 +1,131 @@
var util = require('util');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var Transaction = require('./transaction').Transaction;
var Amount = require('./amount').Amount;
var utils = require('./utils');
var Transaction = require('./transaction').Transaction;
var log = require('./log').internal.sub('server');
/**
* @constructor Server
*
* @param {Remote} Reference to a Remote object
* @param {Object} Options
*
* host: String
* port: String or Number
* secure: Boolean
* @param {String} host
* @param {Number|String} port
* @param [Boolean] securec
*/
function Server(remote, opts) {
EventEmitter.call(this);
var self = this;
if (typeof opts === 'string') {
var parsedUrl = url.parse(opts);
opts = {
host: parsedUrl.hostname,
port: parsedUrl.port,
secure: (parsedUrl.protocol === 'ws:') ? false : true
};
}
if (typeof opts !== 'object') {
throw new TypeError('Server configuration is not an Object');
}
if (!opts.host) opts.host = opts.websocket_ip;
if (!opts.port) opts.port = opts.websocket_port;
if (!opts.secure) opts.secure = opts.websocket_ssl;
if (typeof opts.secure === 'undefined') {
opts.secure = false;
}
var domainRE = /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/;
if (!domainRE.test(opts.host)) {
if (!Server.domainRE.test(opts.host)) {
throw new Error('Server host is malformed, use "host" and "port" server configuration');
}
if (typeof opts.port !== 'number') {
throw new TypeError('Server configuration "port" is not a Number');
// We want to allow integer strings as valid port numbers for backward compatibility
if (!(opts.port = Number(opts.port))) {
throw new TypeError('Server port must be a number');
}
if (opts.port < 1 || opts.port > 65535) {
throw new TypeError('Server "port" must be an integer in range 1-65535');
}
if (typeof opts.secure !== 'boolean') {
throw new TypeError('Server "secure" configuration is not a Boolean');
opts.secure = true;
}
var self = this;
this._remote = remote;
this._opts = opts;
this._ws = void(0);
this._remote = remote;
this._opts = opts;
this._host = opts.host;
this._port = opts.port;
this._secure = opts.secure;
this._ws = void(0);
this._connected = false;
this._shouldConnect = false;
this._state = 'offline';
this._id = 0;
this._retry = 0;
this._requests = { };
this._load_base = 256;
this._load_factor = 256;
this._id = 0;
this._retry = 0;
this._requests = { };
this._load_base = 256;
this._load_factor = 256;
this._fee = 10;
this._fee_ref = 10;
this._fee_base = 10;
this._reserve_base = void(0);
this._reserve_inc = void(0);
this._fee_cushion = this._remote.fee_cushion;
this._opts.url = (opts.secure ? 'wss://' : 'ws://') + opts.host + ':' + opts.port;
this._lastLedgerIndex = NaN;
this._lastLedgerClose = NaN;
this.on('message', function(message) {
this._score = 0;
this._scoreWeights = {
ledgerclose: 5,
response: 1
};
this._url = this._opts.url = (this._opts.secure ? 'wss://' : 'ws://')
+ this._opts.host + ':' + this._opts.port;
this.on('message', onMessage);
function onMessage(message) {
self._handleMessage(message);
});
};
this.on('response_subscribe', function(message) {
this.on('response_subscribe', onSubscribeResponse);
function onSubscribeResponse(message) {
self._handleResponseSubscribe(message);
});
function checkServerActivity() {
if (isNaN(self._lastLedgerClose)) return;
var delta = (Date.now() - self._lastLedgerClose);
if (delta > (1000 * 20)) {
self.reconnect();
}
};
function setActivityInterval() {
self._activityInterval = setInterval(checkServerActivity, 1000);
var interval = self._checkActivity.bind(self);
self._activityInterval = setInterval(interval, 1000);
};
this.on('disconnect', function onDisconnect() {
clearInterval(self._activityInterval);
//self.once('ledger_closed', setActivityInterval);
self.once('ledger_closed', setActivityInterval);
});
this.once('ledger_closed', function() {
//setActiviyInterval();
this.once('ledger_closed', setActivityInterval);
this._remote.on('ledger_closed', function(ledger) {
self._updateScore('ledgerclose', ledger);
});
this.on('response_ping', function(message, request) {
self._updateScore('response', request);
});
this.on('load_changed', function(load) {
self._updateScore('loadchange', load);
});
};
util.inherits(Server, EventEmitter);
Server.domainRE = /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|[-_]){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|[-_]){0,61}[0-9A-Za-z])?)*\.?$/;
/**
* Server states that we will treat as the server being online.
*
@@ -117,6 +141,23 @@ Server.onlineStates = [
'full'
];
/**
* This is the final interface between client code and a socket connection to a
* `rippled` server. As such, this is a decent hook point to allow a WebSocket
* interface conforming object to be used as a basis to mock rippled. This
* avoids the need to bind a websocket server to a port and allows a more
* synchronous style of code to represent a client <-> server message sequence.
* We can also use this to log a message sequence to a buffer.
*
* @api private
*/
Server.websocketConstructor = function() {
// We require this late, because websocket shims may be loaded after
// ripple-lib in the browser
return require('ws');
};
/**
* Set server state
*
@@ -126,7 +167,10 @@ Server.onlineStates = [
Server.prototype._setState = function(state) {
if (state !== this._state) {
this._remote.trace && log.info('set_state:', state);
if (this._remote.trace) {
log.info('set_state:', state);
}
this._state = state;
this.emit('state', state);
@@ -143,32 +187,90 @@ Server.prototype._setState = function(state) {
}
};
/**
* Check that server is still active.
*
* Server activity is determined by ledger_closed events.
* Maximum delay to receive a ledger_closed event is 20s.
*
* If server is inactive, reconnect
*
* @api private
*/
Server.prototype._checkActivity = function() {
if (!this._connected) {
return;
}
if (isNaN(this._lastLedgerClose)) {
return;
}
var delta = (Date.now() - this._lastLedgerClose);
if (delta > (1000 * 25)) {
this.reconnect();
}
};
/**
* Server maintains a score for request prioritization.
*
* The score is determined by various data including
* this server's lag to receive ledger_closed events,
* ping response time, and load(fee) change
*
* @param {String} type
* @param {Object} data
* @api private
*/
Server.prototype._updateScore = function(type, data) {
if (!this._connected) {
return;
}
var weight = this._scoreWeights[type] || 1;
switch (type) {
case 'ledgerclose':
// Ledger lag
var delta = data.ledger_index - this._lastLedgerIndex;
if (delta > 0) {
this._score += weight * delta;
}
break;
case 'response':
// Ping lag
var delta = Math.floor((Date.now() - data.time) / 200);
this._score += weight * delta;
break;
case 'loadchange':
// Load/fee change
this._fee = Number(this._computeFee(10));
break;
}
if (this._score > 1e3) {
this.reconnect();
}
};
/**
* Get the remote address for a server.
* Incompatible with ripple-lib client build
*/
Server.prototype._remoteAddress = function() {
try { var address = this._ws._socket.remoteAddress; } catch (e) { }
var address;
try {
address = this._ws._socket.remoteAddress;
} catch (e) {
}
return address;
};
/** This is the final interface between client code and a socket connection to a
* `rippled` server. As such, this is a decent hook point to allow a WebSocket
* interface conforming object to be used as a basis to mock rippled. This
* avoids the need to bind a websocket server to a port and allows a more
* synchronous style of code to represent a client <-> server message sequence.
* We can also use this to log a message sequence to a buffer.
*
* @api private
*/
Server.websocketConstructor = function() {
// We require this late, because websocket shims may be loaded after
// ripple-lib in the browser
return require('ws');
};
/**
* Disconnect from rippled WebSocket server
*
@@ -178,7 +280,9 @@ Server.websocketConstructor = function() {
Server.prototype.disconnect = function() {
this._shouldConnect = false;
this._setState('offline');
if (this._ws) this._ws.close();
if (this._ws) {
this._ws.close();
}
};
/**
@@ -188,8 +292,14 @@ Server.prototype.disconnect = function() {
*/
Server.prototype.reconnect = function() {
var self = this;
function disconnected() {
self.connect();
};
if (this._ws) {
this.once('disconnect', this.connect.bind(this));
this.once('disconnect', disconnected);
this.disconnect();
}
};
@@ -209,12 +319,18 @@ Server.prototype.connect = function() {
// recently received a message from the server and the WebSocket has not
// reported any issues either. If we do fail to ping or the connection drops,
// we will automatically reconnect.
if (this._connected) return;
if (this._connected) {
return;
}
this._remote.trace && log.info('connect:', this._opts.url);
if (this._remote.trace) {
log.info('connect:', this._opts.url);
}
// Ensure any existing socket is given the command to close first.
if (this._ws) this._ws.close();
if (this._ws) {
this._ws.close();
}
var WebSocket = Server.websocketConstructor();
@@ -233,19 +349,20 @@ Server.prototype.connect = function() {
};
ws.onopen = function onOpen() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
self.emit('socket_open');
// Subscribe to events
self.request(self._remote._serverPrepareSubscribe());
self._request(self._remote._serverPrepareSubscribe());
}
};
ws.onerror = function onError(e) {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
self.emit('socket_error');
self._remote.trace && log.info('onerror:', self._opts.url, e.data || e);
if (self._remote.trace) {
log.info('onerror:', self._opts.url, e.data || e);
}
// Most connection errors for WebSockets are conveyed as 'close' events with
// code 1006. This is done for security purposes and therefore unlikely to
@@ -265,11 +382,11 @@ Server.prototype.connect = function() {
}
};
// Failure to open.
ws.onclose = function onClose() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
self._remote.trace && log.info('onclose:', self._opts.url, ws.readyState);
if (self._remote.trace) {
log.info('onclose:', self._opts.url, ws.readyState);
}
self._handleClose();
}
};
@@ -287,16 +404,22 @@ Server.prototype._retryConnect = function() {
this._retry += 1;
var retryTimeout = (this._retry < 40)
? (1000 / 20) // First, for 2 seconds: 20 times per second
// First, for 2 seconds: 20 times per second
? (1000 / 20)
: (this._retry < 40 + 60)
? (1000) // Then, for 1 minute: once per second
// Then, for 1 minute: once per second
? (1000)
: (this._retry < 40 + 60 + 60)
? (10 * 1000) // Then, for 10 minutes: once every 10 seconds
: (30 * 1000); // Then: once every 30 seconds
// Then, for 10 minutes: once every 10 seconds
? (10 * 1000)
// Then: once every 30 seconds
: (30 * 1000);
function connectionRetry() {
if (self._shouldConnect) {
self._remote.trace && log.info('retry', self._opts.url);
if (self._remote.trace) {
log.info('retry', self._opts.url);
}
self.connect();
}
};
@@ -317,8 +440,10 @@ Server.prototype._handleClose = function() {
this.emit('socket_close');
this._setState('offline');
function noOp(){};
// Prevent additional events from this socket
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function noOp() {};
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = noOp;
if (self._shouldConnect) {
this._retryConnect();
@@ -335,68 +460,128 @@ Server.prototype._handleClose = function() {
Server.prototype._handleMessage = function(message) {
var self = this;
try { message = JSON.parse(message); } catch(e) { }
try {
message = JSON.parse(message);
} catch(e) {
}
if (!Server.isValidMessage(message)) return;
if (!Server.isValidMessage(message)) {
this.emit('unexpected', message);
return;
}
switch (message.type) {
case 'ledgerClosed':
this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message);
this._handleLedgerClosed(message);
break;
case 'serverStatus':
// This message is only received when online.
// As we are connected, it is the definitive final state.
this._setState(~(Server.onlineStates.indexOf(message.server_status)) ? 'online' : 'offline');
if (Server.isLoadStatus(message)) {
self.emit('load', message, self);
self._remote.emit('load', message, self);
var loadChanged = ((message.load_base !== self._load_base) ||
(message.load_factor !== self._load_factor));
if (loadChanged) {
self._load_base = message.load_base;
self._load_factor = message.load_factor;
self.emit('load_changed', message, self);
self._remote.emit('load_changed', message, self);
}
}
this._handleServerStatus(message);
break;
case 'response':
// A response to a request.
var request = self._requests[message.id];
delete self._requests[message.id];
if (!request) {
this._remote.trace && log.info('UNEXPECTED:', self._opts.url, message);
} else if (message.status === 'success') {
this._remote.trace && log.info('response:', self._opts.url, message);
request.emit('success', message.result);
[ self, self._remote ].forEach(function(emitter) {
emitter.emit('response_' + request.message.command, message.result, request, message);
});
} else if (message.error) {
this._remote.trace && log.info('error:', self._opts.url, message);
request.emit('error', {
error : 'remoteError',
error_message : 'Remote reported an error.',
remote : message
});
}
this._handleResponse(message);
break;
case 'path_find':
this._remote.trace && log.info('path_find:', self._opts.url, message);
this._handlePathFind(message);
break;
}
};
Server.prototype._handleLedgerClosed = function(message) {
this._lastLedgerIndex = message.ledger_index;
this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message);
};
Server.prototype._handleServerStatus = function(message) {
// This message is only received when online.
// As we are connected, it is the definitive final state.
var isOnline = ~Server.onlineStates.indexOf(message.server_status);
this._setState(isOnline ? 'online' : 'offline');
if (!Server.isLoadStatus(message)) {
return;
}
this.emit('load', message, this);
this._remote.emit('load', message, this);
var loadChanged = message.load_base !== this._load_base
|| message.load_factor !== this._load_factor
if (loadChanged) {
this._load_base = message.load_base;
this._load_factor = message.load_factor;
this.emit('load_changed', message, this);
this._remote.emit('load_changed', message, this);
}
};
Server.prototype._handleResponse = function(message) {
// A response to a request.
var request = this._requests[message.id];
delete this._requests[message.id];
if (!request) {
if (this._remote.trace) {
log.info('UNEXPECTED:', this._opts.url, message);
}
return;
}
if (message.status === 'success') {
if (this._remote.trace) {
log.info('response:', this._opts.url, message);
}
var command = request.message.command;
var result = message.result;
var responseEvent = 'response_' + command;
request.emit('success', result);
[ this, this._remote ].forEach(function(emitter) {
emitter.emit(responseEvent, result, request, message);
});
} else if (message.error) {
if (this._remote.trace) {
log.info('error:', this._opts.url, message);
}
var error = {
error: 'remoteError',
error_message: 'Remote reported an error.',
remote: message
};
request.emit('error', error);
}
};
Server.prototype._handlePathFind = function(message) {
if (this._remote.trace) {
log.info('path_find:', this._opts.url, message);
}
};
/**
* Handle subscription response messages. Subscription response
* messages indicate that a connection to the server is ready
*
* @api private
*/
Server.prototype._handleResponseSubscribe = function(message) {
if (~(Server.onlineStates.indexOf(message.server_status))) {
this._setState('online');
}
if (Server.isLoadStatus(message)) {
this._load_base = message.load_base || 256;
this._load_factor = message.load_factor || 256;
this._fee_ref = message.fee_ref;
this._fee_base = message.fee_base;
this._reserve_base = message.reserve_base;
this._reserve_inc = message.reserve_inc;
}
};
@@ -423,27 +608,6 @@ Server.isLoadStatus = function(message) {
&& (typeof message.load_factor === 'number');
};
/**
* Handle subscription response messages. Subscription response
* messages indicate that a connection to the server is ready
*
* @api private
*/
Server.prototype._handleResponseSubscribe = function(message) {
if (~(Server.onlineStates.indexOf(message.server_status))) {
this._setState('online');
}
if (Server.isLoadStatus(message)) {
this._load_base = message.load_base || 256;
this._load_factor = message.load_factor || 256;
this._fee_ref = message.fee_ref;
this._fee_base = message.fee_base;
this._reserve_base = message.reserve_base;
this._reserve_inc = message.reserve_inc;
}
};
/**
* Send JSON message to rippled WebSocket server
*
@@ -451,9 +615,11 @@ Server.prototype._handleResponseSubscribe = function(message) {
* @api private
*/
Server.prototype.sendMessage = function(message) {
Server.prototype._sendMessage = function(message) {
if (this._ws) {
this._remote.trace && log.info('request:', this._opts.url, message);
if (this._remote.trace) {
log.info('request:', this._opts.url, message);
}
this._ws.send(JSON.stringify(message));
}
};
@@ -468,38 +634,42 @@ Server.prototype.sendMessage = function(message) {
* @api private
*/
Server.prototype.request = function(request) {
Server.prototype._request = function(request) {
var self = this;
// Only bother if we are still connected.
if (!this._ws) {
this._remote.trace && log.info('request: DROPPING:', self._opts.url, request.message);
if (this._remote.trace) {
log.info('request: DROPPING:', self._opts.url, request.message);
}
return;
}
request.server = this;
request.message.id = this._id;
request.time = Date.now();
this._requests[request.message.id] = request;
// Advance message ID
this._id++;
function sendRequest() {
self._sendMessage(request.message);
};
if (this._isConnected(request)) {
this.sendMessage(request.message);
sendRequest();
} else {
// XXX There are many ways to make this smarter.
function serverReconnected() {
self.sendMessage(request.message);
}
this.once('connect', serverReconnected);
this.once('connect', sendRequest);
}
};
Server.prototype._isConnected = function(request) {
var isSubscribeRequest = request
&& request.message.command === 'subscribe'
&& this._ws.readyState === 1;
&& request.message.command === 'subscribe'
&& this._ws.readyState === 1;
return this._connected || (this._ws && isSubscribeRequest);
};
@@ -512,18 +682,18 @@ Server.prototype._isConnected = function(request) {
* @api private
*/
Server.prototype.computeFee = function(transaction) {
Server.prototype._computeFee = function(transaction) {
var units;
if (transaction instanceof Transaction) {
units = transaction.feeUnits();
units = transaction._getFeeUnits();
} else if (typeof transaction === 'number') {
units = transaction;
} else {
throw new Error('Invalid argument');
}
return this.feeTx(units).to_json();
return this._feeTx(units).to_json();
};
/**
@@ -535,8 +705,8 @@ Server.prototype.computeFee = function(transaction) {
* @return {Amount} Final fee in XRP for specified number of fee units.
*/
Server.prototype.feeTx = function(units) {
var fee_unit = this.feeTxUnit();
Server.prototype._feeTx = function(units) {
var fee_unit = this._feeTxUnit();
return Amount.from_json(String(Math.ceil(units * fee_unit)));
};
@@ -549,7 +719,7 @@ Server.prototype.feeTx = function(units) {
* @return {Number} Recommended amount for one fee unit as float.
*/
Server.prototype.feeTxUnit = function() {
Server.prototype._feeTxUnit = function() {
var fee_unit = this._fee_base / this._fee_ref;
// Apply load fees
@@ -567,10 +737,10 @@ Server.prototype.feeTxUnit = function() {
* Returns the base reserve with load fees and safety margin applied.
*/
Server.prototype.reserve = function(owner_count) {
Server.prototype._reserve = function(ownerCount) {
var reserve_base = Amount.from_json(String(this._reserve_base));
var reserve_inc = Amount.from_json(String(this._reserve_inc));
var owner_count = owner_count || 0;
var owner_count = ownerCount || 0;
if (owner_count < 0) {
throw new Error('Owner count must not be negative.');

View File

@@ -7,15 +7,15 @@ var UInt256 = require('./uint256').UInt256;
var SerializedObject = require('./serializedobject').SerializedObject;
function SHAMap() {
this.root = new SHAMapTreeNodeInner();
this.root = new SHAMapTreeNodeInner(0);
};
SHAMap.prototype.add_item = function (tag, node, type) {
SHAMap.prototype.add_item = function(tag, node, type) {
var node = new SHAMapTreeNodeLeaf(tag, node, type);
this.root.add_item(tag, node);
};
SHAMap.prototype.hash = function () {
SHAMap.prototype.hash = function() {
return this.root.hash();
};
@@ -24,96 +24,100 @@ SHAMap.prototype.hash = function () {
*
* Can be either SHAMapTreeNodeInner or SHAMapTreeNodeLeaf.
*/
function SHAMapTreeNode() {
};
function SHAMapTreeNode() { };
SHAMapTreeNode.TYPE_INNER = 1;
SHAMapTreeNode.TYPE_TRANSACTION_NM = 2;
SHAMapTreeNode.TYPE_TRANSACTION_MD = 3;
SHAMapTreeNode.TYPE_ACCOUNT_STATE = 4;
SHAMapTreeNode.prototype.add_item = function (tag_segment, node) {
throw new Error("Called unimplemented virtual method SHAMapTreeNode#add_item.");
/**
* @param tag {String} 64 hexadecimal characters
*/
SHAMapTreeNode.prototype.add_item = function(tag, node) {
throw new Error('Called unimplemented virtual method SHAMapTreeNode#add_item.');
};
SHAMapTreeNode.prototype.hash = function () {
throw new Error("Called unimplemented virtual method SHAMapTreeNode#hash.");
SHAMapTreeNode.prototype.hash = function() {
throw new Error('Called unimplemented virtual method SHAMapTreeNode#hash.');
};
/**
* Inner (non-leaf) node in a SHAMap tree.
*/
function SHAMapTreeNodeInner() {
function SHAMapTreeNodeInner(depth) {
SHAMapTreeNode.call(this);
this.leaves = {};
this.type = SHAMapTreeNode.INNER;
this.depth = depth == null ? 0 : depth;
this.empty = true;
}
util.inherits(SHAMapTreeNodeInner, SHAMapTreeNode);
SHAMapTreeNodeInner.prototype.add_item = function (tag_segment, node) {
var current_node = this.get_node(tag_segment[0]);
/**
* @param tag {String} (equates to a ledger entries `index`)
*/
SHAMapTreeNodeInner.prototype.add_item = function (tag, node) {
var depth = this.depth;
var existing_node = this.get_node(tag[depth]);
if (current_node) {
if (existing_node) {
// A node already exists in this slot
if (current_node instanceof SHAMapTreeNodeInner) {
if (existing_node instanceof SHAMapTreeNodeInner) {
// There is an inner node, so we need to go deeper
current_node.add_item(tag_segment.slice(1), node);
} else if (current_node.get_segment() === tag_segment) {
existing_node.add_item(tag, node);
} else if (existing_node.tag === tag) {
// Collision
throw new Error("Tried to add a node to a SHAMap that was already in there.");
throw new Error('Tried to add a node to a SHAMap that was already in there.');
} else {
// Turn it into an inner node
var new_inner_node = new SHAMapTreeNodeInner();
var new_inner_node = new SHAMapTreeNodeInner(depth + 1);
// Move the existing leaf node down one level
current_node.set_segment(current_node.get_segment().slice(1));
new_inner_node.set_node(current_node.get_segment()[0], current_node);
// Add the new node next to it
new_inner_node.add_item(tag_segment.slice(1), node);
// Parent new and existing node
new_inner_node.add_item(existing_node.tag, existing_node);
new_inner_node.add_item(tag, node);
// And place the newly created inner node in the slot
this.set_node(tag_segment[0], new_inner_node);
this.set_node(tag[depth], new_inner_node);
}
} else {
// Neat, we have a nice open spot for the new node
node.set_segment(tag_segment);
this.set_node(tag_segment[0], node);
this.set_node(tag[depth], node);
}
};
/**
* Overwrite the node that is currently in a given slot.
*/
SHAMapTreeNodeInner.prototype.set_node = function (slot, node) {
SHAMapTreeNodeInner.prototype.set_node = function(slot, node) {
this.leaves[slot] = node;
this.empty = false;
};
SHAMapTreeNodeInner.prototype.get_node = function (slot) {
SHAMapTreeNodeInner.prototype.get_node = function(slot) {
return this.leaves[slot];
};
SHAMapTreeNodeInner.prototype.hash = function () {
SHAMapTreeNodeInner.prototype.hash = function() {
if (this.empty) {
return UInt256.from_hex(UInt256.HEX_ZERO);
}
var hash_buffer = new SerializedObject();
var buffer = [];
for (var i = 0; i < 16; i++) {
var buffer = [ ];
for (var i=0; i<16; i++) {
var leafHash = UInt256.from_hex(UInt256.HEX_ZERO);
var slot = i.toString(16).toUpperCase();
if ("object" === typeof this.leaves[slot]) {
if (typeof this.leaves[slot] === 'object') {
leafHash = this.leaves[slot].hash();
}
hash_buffer.append(leafHash.to_bytes());
}
@@ -128,42 +132,33 @@ SHAMapTreeNodeInner.prototype.hash = function () {
function SHAMapTreeNodeLeaf(tag, node, type) {
SHAMapTreeNode.call(this);
if ("string" === typeof tag) {
tag = UInt256.from_hex(tag);
} else if (tag instanceof UInt256) {
// Type is already the right one
} else {
throw new Error("Tag is unexpected type.");
if (typeof tag !== 'string') {
throw new Error('Tag is unexpected type.');
}
this.tag = tag;
this.tag_segment = null;
this.tag_bytes = UInt256.from_hex(this.tag).to_bytes();
this.type = type;
this.node = node;
}
};
util.inherits(SHAMapTreeNodeLeaf, SHAMapTreeNode);
SHAMapTreeNodeLeaf.prototype.get_segment = function (segment) {
return this.tag_segment;
};
SHAMapTreeNodeLeaf.prototype.set_segment = function (segment) {
this.tag_segment = segment;
};
SHAMapTreeNodeLeaf.prototype.hash = function () {
var buffer = new SerializedObject();
switch (this.type) {
case SHAMapTreeNode.TYPE_ACCOUNT_STATE:
buffer.append(this.node);
buffer.append(this.tag_bytes);
return buffer.hash(hashprefixes.HASH_LEAF_NODE);
case SHAMapTreeNode.TYPE_TRANSACTION_NM:
return this.tag;
return this.tag_bytes;
case SHAMapTreeNode.TYPE_TRANSACTION_MD:
buffer.append(this.node);
buffer.append(this.tag.to_bytes());
buffer.append(this.tag_bytes);
return buffer.hash(hashprefixes.HASH_TX_NODE);
default:
throw new Error("Tried to hash a SHAMap node of unknown type.");
throw new Error('Tried to hash a SHAMap node of unknown type.');
}
};

View File

@@ -0,0 +1,203 @@
var Crypt = require('./crypt').Crypt;
var Message = require('./message').Message;
var parser = require("url");
var querystring = require('querystring');
var extend = require("extend");
var SignedRequest = function (config) {
// XXX Constructor should be generalized and constructing from an Angular.js
// $http config should be a SignedRequest.from... utility method.
this.config = extend(true, {}, config);
if (!this.config.data) this.config.data = {};
};
/**
* Create a string from request parameters that
* will be used to sign a request
* @param {Object} parsed - parsed url
* @param {Object} date
* @param {Object} mechanism - type of signing
*/
SignedRequest.prototype.getStringToSign = function (parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(this.config.data));
// Canonical request using Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
var canonicalRequest = [
this.config.method || 'GET',
parsed.pathname || '',
parsed.search || '',
// XXX Headers signing not supported
'',
'',
Crypt.hashSha512(canonicalData).toLowerCase()
].join('\n');
// String to sign inspired by Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
//
// We don't have a credential scope, so we skip it.
//
// But that modifies the format, so the format ID is RIPPLE1, instead of AWS4.
return [
mechanism,
date,
Crypt.hashSha512(canonicalRequest).toLowerCase()
].join('\n');
};
//prepare for signing
function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) {
var newObj = {};
var keysSorted = Object.keys(object).sort();
var key;
for (var i in keysSorted) {
key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]);
}
}
return newObj;
} else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys);
} else {
return object;
}
}
//from npm extend
function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval)
return false;
var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
return false;
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key );
};
/**
* HMAC signed request
* @param {Object} config
* @param {Object} auth_secret
* @param {Object} blob_id
*/
SignedRequest.prototype.signHmac = function (auth_secret, blob_id) {
var config = extend(true, {}, this.config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-HMAC-SHA512';
var stringToSign = this.getStringToSign(parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
/**
* Asymmetric signed request
* @param {Object} config
* @param {Object} secretKey
* @param {Object} account
* @param {Object} blob_id
*/
SignedRequest.prototype.signAsymmetric = function (secretKey, account, blob_id) {
var config = extend(true, {}, this.config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = this.getStringToSign(parsed, date, signatureType);
var signature = Message.signMessage(stringToSign, secretKey);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_account: account,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
/**
* Asymmetric signed request for vault recovery
* @param {Object} config
* @param {Object} secretKey
* @param {Object} username
*/
SignedRequest.prototype.signAsymmetricRecovery = function (secretKey, username) {
var config = extend(true, {}, this.config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = this.getStringToSign(parsed, date, signatureType);
var signature = Message.signMessage(stringToSign, secretKey);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_username: username,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
var dateAsIso8601 = (function () {
function pad(n) {
return (n < 0 || n > 9 ? "" : "0") + n;
}
return function dateAsIso8601() {
var date = new Date();
return date.getUTCFullYear() + "-" +
pad(date.getUTCMonth() + 1) + "-" +
pad(date.getUTCDate()) + "T" +
pad(date.getUTCHours()) + ":" +
pad(date.getUTCMinutes()) + ":" +
pad(date.getUTCSeconds()) + ".000Z";
};
})();
// XXX Add methods for verifying requests
// SignedRequest.prototype.verifySignatureHmac
// SignedRequest.prototype.verifySignatureAsymetric
exports.SignedRequest = SignedRequest;

View File

@@ -44,6 +44,7 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var utils = require('./utils');
var sjcl = require('./utils').sjcl;
var Amount = require('./amount').Amount;
var Currency = require('./amount').Currency;
@@ -57,7 +58,9 @@ var config = require('./config');
function Transaction(remote) {
EventEmitter.call(this);
var self = this;
var self = this;
var remote = remote || { };
this.remote = remote;
@@ -66,41 +69,34 @@ function Transaction(remote) {
this._secret = void(0);
this._build_path = false;
this._maxFee = this.remote.max_fee;
this.state = 'unsubmitted';
this.finalized = false;
this.previousSigningHash = void(0);
// Index at which transaction was submitted
this.submitIndex = void(0);
// Canonical signing setting defaults to the Remote's configuration
this.canonical = (typeof remote === 'object') ? !!remote.canonical_signing : true;
// We aren't clever enough to eschew preventative measures so we keep an array
// of all submitted transactionIDs (which can change due to load_factor
// effecting the Fee amount). This should be populated with a transactionID
// any time it goes on the network
this.submittedIDs = [ ]
function finalize(message) {
if (self.result) {
self.result.ledger_index = message.ledger_index;
self.result.ledger_hash = message.ledger_hash;
} else {
self.result = message;
self.result.tx_json = self.tx_json;
}
self.emit('cleanup', message);
};
this.submittedIDs = [ ];
this.once('success', function(message) {
self.finalized = true;
self.finalize(message);
self.setState('validated');
finalize(message);
self.emit('cleanup', message);
});
this.once('error', function(message) {
self.finalized = true;
self.finalize(message);
self.setState('failed');
finalize(message);
self.emit('cleanup', message);
});
this.once('submitted', function() {
@@ -120,6 +116,11 @@ Transaction.fee_units = {
};
Transaction.flags = {
// Universal flags can apply to any transaction type
Universal: {
FullyCanonicalSig: 0x80000000
},
AccountSet: {
RequireDestTag: 0x00010000,
OptionalDestTag: 0x00020000,
@@ -149,6 +150,18 @@ Transaction.flags = {
}
};
// The following are integer (as opposed to bit) flags
// that can be set for particular transactions in the
// SetFlag or ClearFlag field
Transaction.set_clear_flags = {
AccountSet: {
asfRequireDest: 1,
asfRequireAuth: 2,
asfDisallowXRP: 3,
asfDisableMaster: 4
}
};
Transaction.formats = require('./binformat').tx;
Transaction.prototype.consts = {
@@ -205,6 +218,20 @@ Transaction.prototype.setState = function(state) {
}
};
Transaction.prototype.finalize = function(message) {
this.finalized = true;
if (this.result) {
this.result.ledger_index = message.ledger_index;
this.result.ledger_hash = message.ledger_hash;
} else {
this.result = message;
this.result.tx_json = this.tx_json;
}
return this;
};
Transaction.prototype._accountSecret = function(account) {
return this.remote.secrets[account];
};
@@ -221,32 +248,48 @@ Transaction.prototype._accountSecret = function(account) {
* @return {Number} Number of fee units for this transaction.
*/
Transaction.prototype.getFee =
Transaction.prototype._getFeeUnits =
Transaction.prototype.feeUnits = function() {
return Transaction.fee_units['default'];
};
/**
* Get the server whose fee is currently the lowest
* Compute median server fee
*/
Transaction.prototype._getServer = function() {
var self = this;
Transaction.prototype._computeFee = function() {
var servers = this.remote._servers;
var fee = Infinity;
var result;
var fees = [ ];
for (var i=0; i<servers.length; i++) {
var server = servers[i];
if (!server._connected) continue;
var n = server.computeFee(this);
if (n < fee) {
result = server;
fee = n;
if (server._connected) {
fees.push(Number(server._computeFee(this)));
}
}
return result;
switch (fees.length) {
case 0: return;
case 1: return String(fees[0]);
}
fees.sort(function ascending(a, b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
});
var midInd = Math.floor(fees.length / 2);
var median = fees.length % 2 === 0
? Math.floor(0.5 + (fees[midInd] + fees[midInd - 1]) / 2)
: fees[midInd];
return String(median);
};
/**
@@ -258,24 +301,53 @@ Transaction.prototype._getServer = function() {
*/
Transaction.prototype.complete = function() {
if (this.remote) {
if (!this.remote.trusted && !this.remote.local_signing) {
this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server'));
return false;
}
}
// Try to auto-fill the secret
if (!this._secret && !(this._secret = this._account_secret(this.tx_json.Account))) {
return this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
if (!this._secret && !(this._secret = this._accountSecret(this.tx_json.Account))) {
this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
return false;
}
if (typeof this.tx_json.SigningPubKey === 'undefined') {
try {
var seed = Seed.from_json(this._secret);
var key = seed.get_key(this.tx_json.Account);
this.tx_json.SigningPubKey = key.to_hex_pub();
} catch(e) {
this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret'));
return false;
}
}
// If the Fee hasn't been set, one needs to be computed by
// an assigned server
if (this.remote && typeof this.tx_json.Fee === 'undefined') {
if (this.remote.local_fee || !this.remote.trusted) {
this._server = this._getServer();
this.tx_json.Fee = this._server.computeFee(this);
if (!(this.tx_json.Fee = this._computeFee())) {
this.emit('error', new RippleError('tejUnconnected'));
return;
}
}
}
if (typeof this.tx_json.SigningPubKey === 'undefined') {
var seed = Seed.from_json(this._secret);
var key = seed.get_key(this.tx_json.Account);
this.tx_json.SigningPubKey = key.to_hex_pub();
if (Number(this.tx_json.Fee) > this._maxFee) {
this.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded'));
return false;
}
// Set canonical flag - this enables canonicalized signature checking
if (this.remote && this.remote.local_signing && this.canonical) {
this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig;
// JavaScript converts operands to 32-bit signed ints before doing bitwise
// operations. We need to convert it back to an unsigned int.
this.tx_json.Flags = this.tx_json.Flags >>> 0;
}
return this.tx_json;
@@ -296,7 +368,7 @@ Transaction.prototype.hash = function(prefix, as_uint256) {
}
prefix = hashprefixes[prefix];
} else if (!prefix) {
prefix = hashprefixes['HASH_TX_ID'];
prefix = hashprefixes.HASH_TX_ID;
}
var hash = SerializedObject.from_json(this.tx_json).hash(prefix);
@@ -304,7 +376,8 @@ Transaction.prototype.hash = function(prefix, as_uint256) {
return as_uint256 ? hash : hash.to_hex();
};
Transaction.prototype.sign = function() {
Transaction.prototype.sign = function(callback) {
var callback = typeof callback === 'function' ? callback : function(){};
var seed = Seed.from_json(this._secret);
var prev_sig = this.tx_json.TxnSignature;
@@ -315,6 +388,7 @@ Transaction.prototype.sign = function() {
// If the hash is the same, we can re-use the previous signature
if (prev_sig && hash === this.previousSigningHash) {
this.tx_json.TxnSignature = prev_sig;
callback();
return this;
}
@@ -325,6 +399,8 @@ Transaction.prototype.sign = function() {
this.tx_json.TxnSignature = hex;
this.previousSigningHash = hash;
callback();
return this;
};
@@ -349,7 +425,9 @@ Transaction.prototype.findId = function(cache) {
for (var i=0; i<this.submittedIDs.length; i++) {
var hash = this.submittedIDs[i];
if (result = cache[hash]) break;
if ((result = cache[hash])) {
break;
}
}
return result;
@@ -378,7 +456,9 @@ Transaction.prototype.destinationTag = function(tag) {
Transaction.prototype.invoiceID = function(id) {
if (typeof id === 'string') {
while (id.length < 64) id += '0';
while (id.length < 64) {
id += '0';
}
this.tx_json.InvoiceID = id;
}
return this;
@@ -400,7 +480,11 @@ Transaction.prototype.lastLedger = function(sequence) {
};
Transaction._pathRewrite = function(path) {
return path.map(function(node) {
if (!Array.isArray(path)) {
return;
}
var newPath = path.map(function(node) {
var newNode = { };
if (node.hasOwnProperty('account')) {
@@ -419,8 +503,10 @@ Transaction._pathRewrite = function(path) {
newNode.type_hex = node.type_hex;
}
return newNode
return newNode;
});
return newPath;
};
Transaction.prototype.pathAdd = function(path) {
@@ -479,22 +565,25 @@ Transaction.prototype.transferRate = function(rate) {
// Add flags to a transaction.
// --> flags: undefined, _flag_, or [ _flags_ ]
Transaction.prototype.setFlags = function(flags) {
if (!flags) return this;
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
var flag_set = Array.isArray(flags) ? flags : Array.prototype.slice.call(arguments);
// We plan to not define this field on new Transaction.
if (this.tx_json.Flags === void(0)) {
this.tx_json.Flags = 0;
if (flags === void(0)) {
return this;
}
if (typeof flags === 'number') {
this.tx_json.Flags = flags;
return this;
}
var flag_set = Array.isArray(flags) ? flags : Array.prototype.slice.call(arguments);
var transaction_flags = Transaction.flags[this.tx_json.TransactionType] || { };
for (var i=0, l=flag_set.length; i<l; i++) {
var flag = flag_set[i];
if (transaction_flags.hasOwnProperty(flag)) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Immediately report an error or mark it.
return this.emit('error', new RippleError('tejInvalidFlag'));
}
}
@@ -508,10 +597,31 @@ Transaction.prototype.setFlags = function(flags) {
// .transfer_rate()
// .wallet_locator() NYI
// .wallet_size() NYI
Transaction.prototype.accountSet = function(src) {
/**
* Construct an 'AccountSet' transaction.
*
* Note that bit flags can be set using the .setFlags() method
* but for 'AccountSet' transactions there is an additional way to
* modify AccountRoot flags. The values available for the SetFlag
* and ClearFlag are as follows:
*
* "asfRequireDest"
* Require a destination tag
* "asfRequireAuth"
* Authorization is required to extend trust
* "asfDisallowXRP"
* XRP should not be sent to this account
* "asfDisableMaster"
* Disallow use of the master key
*/
Transaction.prototype.accountSet = function(src, set_flag, clear_flag) {
if (typeof src === 'object') {
var options = src;
src = options.source || options.from;
src = options.source || options.from || options.account;
set_flag = options.set_flag || options.set;
clear_flag = options.clear_flag || options.clear;
}
if (!UInt160.is_valid(src)) {
@@ -520,6 +630,21 @@ Transaction.prototype.accountSet = function(src) {
this.tx_json.TransactionType = 'AccountSet';
this.tx_json.Account = UInt160.json_rewrite(src);
var SetClearFlags = Transaction.set_clear_flags.AccountSet;
function prepareFlag(flag) {
return (typeof flag === 'number') ? flag : (SetClearFlags[flag] || SetClearFlags['asf' + flag]);
};
if (set_flag && (set_flag = prepareFlag(set_flag))) {
this.tx_json.SetFlag = set_flag;
}
if (clear_flag && (clear_flag = prepareFlag(clear_flag))) {
this.tx_json.ClearFlag = clear_flag;
}
return this;
};
@@ -529,13 +654,14 @@ Transaction.prototype.claim = function(src, generator, public_key, signature) {
signature = options.signature;
public_key = options.public_key;
generator = options.generator;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
this.tx_json.TransactionType = 'Claim';
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
@@ -543,7 +669,7 @@ Transaction.prototype.offerCancel = function(src, sequence) {
if (typeof src === 'object') {
var options = src;
sequence = options.sequence;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -553,6 +679,7 @@ Transaction.prototype.offerCancel = function(src, sequence) {
this.tx_json.TransactionType = 'OfferCancel';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.OfferSequence = Number(sequence);
return this;
};
@@ -567,7 +694,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira
expiration = options.expiration;
taker_gets = options.taker_gets || options.sell;
taker_pays = options.taker_pays || options.buy;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -580,15 +707,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira
this.tx_json.TakerGets = Amount.json_rewrite(taker_gets);
if (expiration) {
switch (expiration.constructor) {
case Date:
//offset = (new Date(2000, 0, 1).getTime()) - (new Date(1970, 0, 1).getTime());
this.tx_json.Expiration = expiration.getTime() - 946684800000;
break;
case Number:
this.tx_json.Expiration = expiration;
break;
}
this.tx_json.Expiration = utils.time.toRipple(expiration);
}
if (cancel_sequence) {
@@ -598,41 +717,34 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira
return this;
};
Transaction.prototype.passwordFund = function(src, dst) {
/**
* Construct a 'SetRegularKey' transaction.
* If the RegularKey is set, the private key that corresponds to
* it can be used to sign transactions instead of the master key
*
* The RegularKey must be a valid Ripple Address, or a Hash160 of
* the public key corresponding to the new private signing key.
*/
Transaction.prototype.setRegularKey = function(src, regular_key) {
if (typeof src === 'object') {
var options = src;
dst = options.destination || options.to;
src = options.source || options.from;
}
if (!UInt160.is_valid(dst)) {
throw new Error('Destination address invalid');
}
this.tx_json.TransactionType = 'PasswordFund';
this.tx_json.Destination = UInt160.json_rewrite(dst);
return this;
};
Transaction.prototype.passwordSet = function(src, authorized_key, generator, public_key, signature) {
if (typeof src === 'object') {
var options = src;
signature = options.signature;
public_key = options.public_key;
generator = options.generator;
authorized_key = options.authorized_key;
src = options.source || options.from;
src = options.address || options.account || options.from;
regular_key = options.regular_key;
}
if (!UInt160.is_valid(src)) {
throw new Error('Source address invalid');
}
this.tx_json.TransactionType = 'PasswordSet';
this.tx_json.RegularKey = authorized_key;
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
if (!UInt160.is_valid(regular_key)) {
throw new Error('RegularKey must be a valid Ripple Address (a Hash160 of the public key)');
}
this.tx_json.TransactionType = 'SetRegularKey';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.RegularKey = UInt160.json_rewrite(regular_key);
return this;
};
@@ -658,10 +770,7 @@ Transaction.prototype.payment = function(src, dst, amount) {
var options = src;
amount = options.amount;
dst = options.destination || options.to;
src = options.source || options.from;
if (options.invoiceID) {
this.invoiceID(options.invoiceID);
}
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -672,10 +781,6 @@ Transaction.prototype.payment = function(src, dst, amount) {
throw new Error('Payment destination address invalid');
}
if (/^[\d]+[A-Z]{3}$/.test(amount)) {
amount = Amount.from_human(amount);
}
this.tx_json.TransactionType = 'Payment';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.Amount = Amount.json_rewrite(amount);
@@ -691,7 +796,7 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o
quality_out = options.quality_out;
quality_in = options.quality_in;
limit = options.limit;
src = options.source || options.from;
src = options.source || options.from || options.account;
}
if (!UInt160.is_valid(src)) {
@@ -718,28 +823,6 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o
return this;
};
Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_key, signature) {
if (typeof src === 'object') {
var options = src;
signature = options.signature;
public_key = options.public_key;
authorized_key = options.authorized_key;
amount = options.amount;
src = options.source || options.from;
}
if (!UInt160.is_valid(src)) {
throw new Error('Source address invalid');
}
this.tx_json.TransactionType = 'WalletAdd';
this.tx_json.Amount = Amount.json_rewrite(amount);
this.tx_json.RegularKey = authorized_key;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
// Submit a transaction to the network.
Transaction.prototype.submit = function(callback) {
var self = this;
@@ -765,32 +848,24 @@ Transaction.prototype.submit = function(callback) {
var account = this.tx_json.Account;
if (typeof account !== 'string') {
this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
} else {
// YYY Might check paths for invalid accounts.
this.remote.account(account).submit(this);
if (!UInt160.is_valid(account)) {
return this.emit('error', new RippleError('tejInvalidAccount', 'Account is missing or invalid'));
}
// YYY Might check paths for invalid accounts.
this.remote.account(account).submit(this);
return this;
};
Transaction.prototype.transactionManager = function() {
return this.remote.account(this.tx_json.Account)._transactionManager;
};
Transaction.prototype.abort = function(callback) {
var callback = (typeof callback === 'function') ? callback : function(){};
if (!this.finalized) {
var callback = (typeof callback === 'function') ? callback : function(){};
this.once('final', callback);
this.emit('abort');
}
};
Transaction.prototype.iff = function(fn) {
this._iff = fn;
};
Transaction.prototype.summary = function() {
return Transaction.summary.call(this);
};
@@ -805,9 +880,9 @@ Transaction.summary = function() {
initialSubmitIndex: this.initialSubmitIndex,
lastLedgerSequence: this.lastLedgerSequence,
state: this.state,
server: this._server ? this._server._opts.url : void(0),
server: this._server ? this._server._opts.url : void(0),
finalized: this.finalized
}
};
if (this.result) {
result.result = {
@@ -816,7 +891,7 @@ Transaction.summary = function() {
ledger_hash : this.result.ledger_hash,
ledger_index : this.result.ledger_index,
transaction_hash : this.result.tx_json.hash
}
};
}
return result;

View File

@@ -3,6 +3,7 @@ var EventEmitter = require('events').EventEmitter;
var Transaction = require('./transaction').Transaction;
var RippleError = require('./rippleerror').RippleError;
var PendingQueue = require('./transactionqueue').TransactionQueue;
var log = require('./log').internal.sub('transactionmanager');
/**
* @constructor TransactionManager
@@ -20,26 +21,30 @@ function TransactionManager(account) {
this._nextSequence = void(0);
this._maxFee = this._remote.max_fee;
this._submissionTimeout = this._remote._submission_timeout;
this._pending = new PendingQueue;
this._pending = new PendingQueue();
// Query remote server for next account sequence number
this._loadSequence();
function transactionReceived(res) {
var transaction = TransactionManager.normalizeTransaction(res);
var sequence = transaction.transaction.Sequence;
var hash = transaction.transaction.hash;
var sequence = transaction.tx_json.Sequence;
var hash = transaction.tx_json.hash;
if (!transaction.validated) return;
if (!transaction.validated) {
return;
}
self._pending.addReceivedSequence(sequence);
// ND: we need to check against all submissions IDs
var submission = self._pending.getSubmission(hash);
self._remote._trace('transactionmanager: transaction_received:', transaction.transaction);
if (self._remote.trace) {
log.info('transaction received:', transaction.tx_json);
}
if (submission) {
if (submission instanceof Transaction) {
// ND: A `success` handler will `finalize` this later
submission.emit('success', transaction);
} else {
@@ -49,38 +54,13 @@ function TransactionManager(account) {
this._account.on('transaction-outbound', transactionReceived);
function adjustFees(loadData, server) {
// ND: note, that `Fee` is a component of a transactionID
self._pending.forEach(function(pending) {
var shouldAdjust = pending._server === server
&& self._remote.local_fee && pending.tx_json.Fee;
if (!shouldAdjust) return;
var oldFee = pending.tx_json.Fee;
var newFee = server.computeFee(pending);
if (Number(newFee) > self._maxFee) {
return pending.once('presubmit', function() {
pending.emit('error', 'tejMaxFeeExceeded');
});
}
pending.tx_json.Fee = newFee;
pending.emit('fee_adjusted', oldFee, newFee);
self._remote._trace('transactionmanager: adjusting_fees:', pending.tx_json, oldFee, newFee);
});
};
this._remote.on('load_changed', adjustFees);
this._remote.on('load_changed', this._adjustFees.bind(this));
function updatePendingStatus(ledger) {
self._pending.forEach(function(pending) {
switch (ledger.ledger_index - pending.submitIndex) {
case 8:
pending.emit('lost', ledger);
self._remote._trace('transactionmanager: update_pending:', pending.tx_json);
break;
case 4:
pending.emit('missing', ledger);
@@ -104,9 +84,10 @@ function TransactionManager(account) {
ledger_index_min: -1,
ledger_index_max: -1,
binary: true,
parseBinary: true,
limit: 100,
filter: 'outbound'
}
};
function accountTx(err, transactions) {
if (!err && Array.isArray(transactions.transactions)) {
@@ -116,9 +97,7 @@ function TransactionManager(account) {
self._remote.on('ledger_closed', updatePendingStatus);
//Load next transaction sequence
self._loadSequence(function sequenceLoaded() {
self._resubmit();
});
self._loadSequence(self._resubmit.bind(self));
callback();
};
@@ -149,32 +128,75 @@ util.inherits(TransactionManager, EventEmitter);
//Normalize transactions received from account
//transaction stream and account_tx
TransactionManager.normalizeTransaction = function(tx) {
var transaction = tx;
var transaction = { };
Object.keys(tx).forEach(function(key) {
transaction[key] = tx[key];
});
if (!tx.engine_result) {
// account_tx
transaction = {
engine_result: tx.meta.TransactionResult,
transaction: tx.tx,
tx_json: tx.tx,
hash: tx.tx.hash,
ledger_index: tx.tx.ledger_index,
meta: tx.meta,
type: 'transaction',
validated: true
}
transaction.result = transaction.engine_result;
};
transaction.result = transaction.engine_result;
transaction.result_message = transaction.engine_result_message;
}
transaction.metadata = transaction.meta;
if (!transaction.metadata) {
transaction.metadata = transaction.meta;
}
if (!transaction.tx_json) {
transaction.tx_json = transaction.transaction;
}
delete transaction.transaction;
delete transaction.mmeta;
delete transaction.meta;
return transaction;
};
// Transaction fees are adjusted in real-time
TransactionManager.prototype._adjustFees = function(loadData) {
// ND: note, that `Fee` is a component of a transactionID
var self = this;
if (!this._remote.local_fee) {
return;
}
this._pending.forEach(function(pending) {
var oldFee = pending.tx_json.Fee;
var newFee = pending._computeFee();
function maxFeeExceeded() {
pending.once('presubmit', function() {
pending.emit('error', 'tejMaxFeeExceeded');
});
};
if (Number(newFee) > self._maxFee) {
return maxFeeExceeded();
}
pending.tx_json.Fee = newFee;
pending.emit('fee_adjusted', oldFee, newFee);
if (self._remote.trace) {
log.info('fee adjusted:', pending.tx_json, oldFee, newFee);
}
});
};
//Fill an account transaction sequence
TransactionManager.prototype._fillSequence = function(tx, callback) {
var self = this;
@@ -184,25 +206,36 @@ TransactionManager.prototype._fillSequence = function(tx, callback) {
fill.account_set(self._accountID);
fill.tx_json.Sequence = sequence;
fill.once('submitted', callback);
// Secrets may be set on a per-transaction basis
if (tx._secret) {
fill.secret(tx._secret);
}
fill.submit();
};
function sequenceLoaded(err, sequence) {
if (typeof sequence !== 'number') {
callback(new Error('Failed to fetch account transaction sequence'));
return;
return callback(new Error('Failed to fetch account transaction sequence'));
}
var sequenceDif = tx.tx_json.Sequence - sequence;
var submitted = 0;
for (var i=sequence; i<tx.tx_json.Sequence; i++) {
submitFill(i, function() {
;(function nextFill(sequence) {
if (sequence >= tx.tx_json.Sequence) {
return;
}
submitFill(sequence, function() {
if (++submitted === sequenceDif) {
callback();
} else {
nextFill(sequence + 1);
}
});
}
})(sequence);
};
this._loadSequence(sequenceLoaded);
@@ -241,7 +274,9 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) {
var hashCached = pending.findId(self._pending._idCache);
self._remote._trace('transactionmanager: resubmit:', pending.tx_json);
if (self._remote.trace) {
log.info('resubmit:', pending.tx_json);
}
if (hashCached) {
return pending.emit('success', hashCached);
@@ -250,7 +285,10 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) {
while (self._pending.hasSequence(pending.tx_json.Sequence)) {
//Sequence number has been consumed by another transaction
pending.tx_json.Sequence += 1;
self._remote._trace('transactionmanager: incrementing sequence:', pending.tx_json);
if (self._remote.trace) {
log.info('incrementing sequence:', pending.tx_json);
}
}
self._request(pending);
@@ -260,14 +298,18 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) {
;(function nextTransaction(i) {
var transaction = pending[i];
if (!(transaction instanceof Transaction)) return;
if (!(transaction instanceof Transaction)) {
return;
}
transaction.once('submitted', function(m) {
transaction.emit('resubmitted', m);
self._loadSequence();
if (++i < pending.length) nextTransaction(i);
if (++i < pending.length) {
nextTransaction(i);
}
});
resubmitTransaction(transaction);
@@ -278,16 +320,21 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) {
};
TransactionManager.prototype._waitLedgers = function(ledgers, callback) {
if (ledgers < 1) return callback();
if (ledgers < 1) {
return callback();
}
var self = this;
var closes = 0;
function ledgerClosed() {
if (++closes === ledgers) {
self._remote.removeListener('ledger_closed', ledgerClosed);
callback();
if (++closes < ledgers) {
return;
}
self._remote.removeListener('ledger_closed', ledgerClosed);
callback();
};
this._remote.on('ledger_closed', ledgerClosed);
@@ -302,12 +349,6 @@ TransactionManager.prototype._request = function(tx) {
}
if (tx.attempts > 0 && !remote.local_signing) {
// && tx.submittedTxnIDs.length != tx.attempts
// ^^^ Above commented out intentionally
// ^^^^ We might be a bit cleverer about allowing this in SOME cases, but
// it's not really worth it, and would be prone to error. Use
// `local_signing`
var message = ''
+ 'It is not possible to resubmit transactions automatically safely without '
+ 'synthesizing the transactionID locally. See `local_signing` config option';
@@ -317,56 +358,30 @@ TransactionManager.prototype._request = function(tx) {
tx.emit('presubmit');
if (tx.finalized) return;
tx.submitIndex = this._remote._ledger_current_index;
if (tx.attempts === 0) {
tx.initialSubmitIndex = tx.submitIndex;
if (tx.finalized) {
return;
}
if (!tx._setLastLedger) {
// Honor LastLedgerSequence set by user of API. If
// left unset by API, bump LastLedgerSequence
tx.tx_json.LastLedgerSequence = tx.submitIndex + 8;
if (remote.trace) {
log.info('submit transaction:', tx.tx_json);
}
tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence;
var submitRequest = remote.requestSubmit();
if (remote.local_signing) {
tx.sign();
// TODO: We are serializing twice, when we could/should be feeding the
// tx_blob to `tx.hash()` which rebuilds it to sign it.
submitRequest.tx_blob(tx.serialize().to_hex());
// ND: ecdsa produces a random `TxnSignature` field value, a component of
// the hash. Attempting to identify a transaction via a hash synthesized
// locally while using remote signing is inherently flawed.
tx.addId(tx.hash());
} else {
// ND: `build_path` is completely ignored when doing local signing as
// `Paths` is a component of the signed blob, the `tx_blob` is signed,
// sealed and delivered, and the txn unmodified.
// TODO: perhaps an exception should be raised if build_path is attempted
// while local signing
submitRequest.build_path(tx._build_path);
submitRequest.secret(tx._secret);
submitRequest.tx_json(tx.tx_json);
}
remote._trace('transactionmanager: submit:', tx.tx_json);
function transactionProposed(message) {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
// If server is honest, don't expect a final if rejected.
message.rejected = tx.isRejected(message.engine_result_code);
tx.emit('proposed', message);
};
function transactionFailed(message) {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
switch (message.engine_result) {
case 'tefPAST_SEQ':
self._resubmit(1, tx);
@@ -377,25 +392,30 @@ TransactionManager.prototype._request = function(tx) {
};
function transactionRetry(message) {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
self._fillSequence(tx, function() {
self._resubmit(1, tx);
});
};
function transactionFeeClaimed(message) {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
tx.emit('error', message);
};
function transactionFailedLocal(message) {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
var shouldAdjustFee = self._remote.local_fee
&& (message.engine_result === 'telINSUF_FEE_P');
if (shouldAdjustFee) {
self._resubmit(1, tx);
if (self._remote.local_fee && (message.engine_result === 'telINSUF_FEE_P')) {
self._resubmit(2, tx);
} else {
submissionError(message);
}
@@ -403,7 +423,9 @@ TransactionManager.prototype._request = function(tx) {
function submissionError(error) {
// Finalized (e.g. aborted) transactions must stop all activity
if (tx.finalized) return;
if (tx.finalized) {
return;
}
if (TransactionManager._isTooBusy(error)) {
self._resubmit(1, tx);
@@ -415,7 +437,9 @@ TransactionManager.prototype._request = function(tx) {
function submitted(message) {
// Finalized (e.g. aborted) transactions must stop all activity
if (tx.finalized) return;
if (tx.finalized) {
return;
}
// ND: If for some unknown reason our hash wasn't computed correctly this is
// an extra measure.
@@ -427,7 +451,9 @@ TransactionManager.prototype._request = function(tx) {
tx.result = message;
remote._trace('transactionmanager: submit_response:', message);
if (remote.trace) {
log.info('submit response:', message);
}
tx.emit('submitted', message);
@@ -453,24 +479,38 @@ TransactionManager.prototype._request = function(tx) {
}
};
var submitRequest = remote.requestSubmit();
submitRequest.once('error', submitted);
submitRequest.once('success', submitted);
if (tx._server) {
submitRequest.server = tx._server;
}
function prepareSubmit() {
if (remote.local_signing) {
// TODO: We are serializing twice, when we could/should be feeding the
// tx_blob to `tx.hash()` which rebuilds it to sign it.
submitRequest.tx_blob(tx.serialize().to_hex());
// ND: ecdsa produces a random `TxnSignature` field value, a component of
// the hash. Attempting to identify a transaction via a hash synthesized
// locally while using remote signing is inherently flawed.
tx.addId(tx.hash());
} else {
// ND: `build_path` is completely ignored when doing local signing as
// `Paths` is a component of the signed blob, the `tx_blob` is signed,
// sealed and delivered, and the txn unmodified.
// TODO: perhaps an exception should be raised if build_path is attempted
// while local signing
submitRequest.build_path(tx._build_path);
submitRequest.secret(tx._secret);
submitRequest.tx_json(tx.tx_json);
}
if (tx._server) {
submitRequest.server = tx._server;
}
if (typeof tx._iff !== 'function') {
submitTransaction();
} else {
return tx._iff(tx.summary(), function(err, proceed) {
if (err || !proceed) {
tx.emit('abort');
} else {
submitTransaction();
}
});
}
};
function requestTimeout() {
// ND: What if the response is just slow and we get a response that
@@ -483,26 +523,52 @@ TransactionManager.prototype._request = function(tx) {
// that ALL transactionIDs sent over network are tracked.
// Finalized (e.g. aborted) transactions must stop all activity
if (tx.finalized) return;
if (tx.finalized) {
return;
}
tx.emit('timeout');
if (remote._connected) {
remote._trace('transactionmanager: timeout:', tx.tx_json);
if (remote.trace) {
log.info('timeout:', tx.tx_json);
}
self._resubmit(3, tx);
}
};
function submitTransaction() {
if (tx.finalized) return;
if (tx.finalized) {
return;
}
submitRequest.timeout(self._submissionTimeout, requestTimeout);
submitRequest.request();
submitRequest.broadcast();
tx.attempts++;
tx.emit('postsubmit');
};
tx.submitIndex = this._remote._ledger_current_index;
if (tx.attempts === 0) {
tx.initialSubmitIndex = tx.submitIndex;
}
if (!tx._setLastLedger) {
// Honor LastLedgerSequence set by user of API. If
// left unset by API, bump LastLedgerSequence
tx.tx_json.LastLedgerSequence = tx.submitIndex + 8;
}
tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence;
if (remote.local_signing) {
tx.sign(prepareSubmit);
} else {
prepareSubmit();
}
return submitRequest;
};
@@ -532,7 +598,7 @@ TransactionManager._isTooBusy = function(error) {
/**
* Entry point for TransactionManager submission
*
* @param {Object} tx
* @param {Transaction} tx
*/
TransactionManager.prototype.submit = function(tx) {
@@ -540,22 +606,23 @@ TransactionManager.prototype.submit = function(tx) {
var remote = this._remote;
// If sequence number is not yet known, defer until it is.
if (typeof this._nextSequence === 'undefined') {
function sequenceLoaded() {
self.submit(tx);
};
this.once('sequence_loaded', sequenceLoaded);
if (typeof this._nextSequence !== 'number') {
this.once('sequence_loaded', this.submit.bind(this, tx));
return;
}
// Finalized (e.g. aborted) transactions must stop all activity
if (tx.finalized) return;
if (tx.finalized) {
return;
}
function cleanup(message) {
// ND: We can just remove this `tx` by identity
self._pending.remove(tx);
tx.emit('final', message);
remote._trace('transactionmanager: finalize_transaction:', tx.tx_json);
if (remote.trace) {
log.info('transaction finalized:', tx.tx_json, self._pending.getLength());
}
};
tx.once('cleanup', cleanup);
@@ -580,27 +647,22 @@ TransactionManager.prototype.submit = function(tx) {
tx.tx_json.Sequence = this._nextSequence++;
}
// Attach secret, associate transaction with a server, attach fee.
// If the transaction can't complete, decrement sequence so that
// subsequent transactions
if (!tx.complete()) {
this._nextSequence--;
return;
}
tx.attempts = 0;
// Attach secret, associate transaction with a server, attach fee
tx.complete();
var fee = Number(tx.tx_json.Fee);
if (!tx._secret && !tx.tx_json.TxnSignature) {
tx.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
} else if (!remote.trusted && !remote.local_signing) {
tx.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server'));
} else if (fee && fee > this._maxFee) {
tx.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded'));
} else {
// ND: this is the ONLY place we put the tx into the queue. The
// TransactionQueue queue is merely a list, so any mutations to tx._hash
// will cause subsequent look ups (eg. inside 'transaction-outbound'
// validated transaction clearing) to fail.
this._pending.push(tx);
this._request(tx);
}
// ND: this is the ONLY place we put the tx into the queue. The
// TransactionQueue queue is merely a list, so any mutations to tx._hash
// will cause subsequent look ups (eg. inside 'transaction-outbound'
// validated transaction clearing) to fail.
this._pending.push(tx);
this._request(tx);
};
exports.TransactionManager = TransactionManager;

View File

@@ -3,56 +3,13 @@
* Manager for pending transactions
*/
var LRU = require('lru-cache');
var Transaction = require('./transaction').Transaction;
function TransactionQueue() {
var self = this;
this._queue = [ ];
this._idCache = { };
this._sequenceCache = { };
this._save = void(0);
};
TransactionQueue.prototype.clearCache = function() {
this._idCache = { };
this._sequenceCache = { };
};
TransactionQueue.prototype.getMinLedger = function() {
var minLedger = Infinity;
for (var i=0; i<this._queue.length; i++) {
var submitIndex = this._queue[i].submitIndex;
if (typeof submitIndex !== 'number') {
// If any pending transactions don't have a submit index,
// return -1 for scanning all previous transactions
minLedger = -1;
break;
}
if (submitIndex < minLedger) {
minLedger = submitIndex;
}
};
if (!isFinite(minLedger)) minLedger = -1;
if (minLedger !== -1) minLedger -= 1;
return minLedger;
};
TransactionQueue.prototype.save = function() {
if (typeof this._save !== 'function') return;
this._save(this._queue.map(function(tx) {
return {
tx_json: tx.tx_json,
submittedIDs: tx.submittedIDs
}
}));
this._queue = [ ];
this._idCache = LRU();
this._sequenceCache = LRU();
};
/**
@@ -60,23 +17,7 @@ TransactionQueue.prototype.save = function() {
*/
TransactionQueue.prototype.addReceivedSequence = function(sequence) {
this._sequenceCache[sequence] = true;
};
/**
* Store received (validated) ID transaction
*/
TransactionQueue.prototype.addReceivedId = function(id, transaction) {
this._idCache[id] = transaction;
};
/**
* Get received (validated) transaction by ID
*/
TransactionQueue.prototype.getReceived = function(id) {
return this._idCache[id];
this._sequenceCache.set(String(sequence), true);
};
/**
@@ -85,7 +26,23 @@ TransactionQueue.prototype.getReceived = function(id) {
*/
TransactionQueue.prototype.hasSequence = function(sequence) {
return this._sequenceCache[sequence] || false;
return this._sequenceCache.has(String(sequence));
};
/**
* Store received (validated) ID transaction
*/
TransactionQueue.prototype.addReceivedId = function(id, transaction) {
this._idCache.set(id, transaction);
};
/**
* Get received (validated) transaction by ID
*/
TransactionQueue.prototype.getReceived = function(id) {
return this._idCache.get(id);
};
/**
@@ -93,10 +50,10 @@ TransactionQueue.prototype.hasSequence = function(sequence) {
* may have multiple associated IDs.
*/
TransactionQueue.prototype.getSubmission = function(id, callback) {
var result = false;
TransactionQueue.prototype.getSubmission = function(id) {
var result = void(0);
for (var i=0, tx; tx=this._queue[i]; i++) {
for (var i=0, tx; (tx=this._queue[i]); i++) {
if (~tx.submittedIDs.indexOf(id)) {
result = tx;
break;
@@ -114,30 +71,32 @@ TransactionQueue.prototype.remove = function(tx) {
// ND: We are just removing the Transaction by identity
var i = this._queue.length;
if (typeof tx === 'string') {
tx = this.getSubmission(tx);
}
if (!(tx instanceof Transaction)) {
return;
}
while (i--) {
if (this._queue[i] === tx) {
this._queue.splice(i, 1);
break;
}
}
if (!this._queue.length) {
this.clearCache();
}
this.save();
};
TransactionQueue.prototype.push = function(tx) {
this._queue.push(tx);
this.save();
};
TransactionQueue.prototype.forEach = function(fn) {
this._queue.forEach(fn);
};
TransactionQueue.prototype.length = function() {
TransactionQueue.prototype.length =
TransactionQueue.prototype.getLength = function() {
return this._queue.length;
};

View File

@@ -4,27 +4,25 @@ var config = require('./config');
var BigInteger = utils.jsbn.BigInteger;
var Base = require('./base').Base;
//
// Abstract UInt class
//
// Base class for UInt??? classes
// Base class for UInt classes
//
var UInt = function () {
var UInt = function() {
// Internal form: NaN or BigInteger
this._value = NaN;
this._update();
};
UInt.json_rewrite = function (j, opts) {
UInt.json_rewrite = function(j, opts) {
return this.from_json(j).to_json(opts);
};
// Return a new UInt from j.
UInt.from_generic = function (j) {
UInt.from_generic = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -33,7 +31,7 @@ UInt.from_generic = function (j) {
};
// Return a new UInt from j.
UInt.from_hex = function (j) {
UInt.from_hex = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -42,7 +40,7 @@ UInt.from_hex = function (j) {
};
// Return a new UInt from j.
UInt.from_json = function (j) {
UInt.from_json = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -51,7 +49,7 @@ UInt.from_json = function (j) {
};
// Return a new UInt from j.
UInt.from_bits = function (j) {
UInt.from_bits = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -60,7 +58,7 @@ UInt.from_bits = function (j) {
};
// Return a new UInt from j.
UInt.from_bytes = function (j) {
UInt.from_bytes = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -69,7 +67,7 @@ UInt.from_bytes = function (j) {
};
// Return a new UInt from j.
UInt.from_bn = function (j) {
UInt.from_bn = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -78,7 +76,7 @@ UInt.from_bn = function (j) {
};
// Return a new UInt from j.
UInt.from_number = function (j) {
UInt.from_number = function(j) {
if (j instanceof this) {
return j.clone();
} else {
@@ -86,32 +84,34 @@ UInt.from_number = function (j) {
}
};
UInt.is_valid = function (j) {
UInt.is_valid = function(j) {
return this.from_json(j).is_valid();
};
UInt.prototype.clone = function () {
UInt.prototype.clone = function() {
return this.copyTo(new this.constructor());
};
// Returns copy.
UInt.prototype.copyTo = function (d) {
UInt.prototype.copyTo = function(d) {
d._value = this._value;
if ("function" === typeof d._update) d._update();
if (typeof d._update === 'function') {
d._update();
}
return d;
};
UInt.prototype.equals = function (d) {
UInt.prototype.equals = function(d) {
return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value);
};
UInt.prototype.is_valid = function () {
UInt.prototype.is_valid = function() {
return this._value instanceof BigInteger;
};
UInt.prototype.is_zero = function () {
UInt.prototype.is_zero = function() {
return this._value.equals(BigInteger.ZERO);
};
@@ -125,47 +125,44 @@ UInt.prototype.is_zero = function () {
* The reason for keeping this mechanism in this class is so every subclass can
* call it whenever it modifies the internal state.
*/
UInt.prototype._update = function () {
UInt.prototype._update = function() {
// Nothing to do by default. Subclasses will override this.
};
// value = NaN on error.
UInt.prototype.parse_generic = function (j) {
UInt.prototype.parse_generic = function(j) {
// Canonicalize and validate
if (config.accounts && j in config.accounts)
if (config.accounts && (j in config.accounts)) {
j = config.accounts[j].account;
}
switch (j) {
case undefined:
case "0":
case this.constructor.STR_ZERO:
case this.constructor.ACCOUNT_ZERO:
case this.constructor.HEX_ZERO:
this._value = BigInteger.valueOf();
break;
case undefined:
case '0':
case this.constructor.STR_ZERO:
case this.constructor.ACCOUNT_ZERO:
case this.constructor.HEX_ZERO:
this._value = BigInteger.valueOf();
break;
case "1":
case this.constructor.STR_ONE:
case this.constructor.ACCOUNT_ONE:
case this.constructor.HEX_ONE:
this._value = new BigInteger([1]);
case '1':
case this.constructor.STR_ONE:
case this.constructor.ACCOUNT_ONE:
case this.constructor.HEX_ONE:
this._value = new BigInteger([1]);
break;
break;
default:
if ('string' !== typeof j) {
this._value = NaN;
}
else if (this.constructor.width === j.length) {
this._value = new BigInteger(utils.stringToArray(j), 256);
}
else if ((this.constructor.width*2) === j.length) {
// XXX Check char set!
this._value = new BigInteger(j, 16);
}
else {
this._value = NaN;
}
default:
if (typeof j !== 'string') {
this._value = NaN;
} else if (this.constructor.width === j.length) {
this._value = new BigInteger(utils.stringToArray(j), 256);
} else if ((this.constructor.width * 2) === j.length) {
// XXX Check char set!
this._value = new BigInteger(j, 16);
} else {
this._value = NaN;
}
}
this._update();
@@ -173,12 +170,11 @@ UInt.prototype.parse_generic = function (j) {
return this;
};
UInt.prototype.parse_hex = function (j) {
if ('string' === typeof j &&
j.length === (this.constructor.width * 2)) {
this._value = new BigInteger(j, 16);
UInt.prototype.parse_hex = function(j) {
if (typeof j === 'string' && j.length === (this.constructor.width * 2)) {
this._value = new BigInteger(j, 16);
} else {
this._value = NaN;
this._value = NaN;
}
this._update();
@@ -186,7 +182,7 @@ UInt.prototype.parse_hex = function (j) {
return this;
};
UInt.prototype.parse_bits = function (j) {
UInt.prototype.parse_bits = function(j) {
if (sjcl.bitArray.bitLength(j) !== this.constructor.width * 8) {
this._value = NaN;
} else {
@@ -200,11 +196,11 @@ UInt.prototype.parse_bits = function (j) {
};
UInt.prototype.parse_bytes = function (j) {
UInt.prototype.parse_bytes = function(j) {
if (!Array.isArray(j) || j.length !== this.constructor.width) {
this._value = NaN;
this._value = NaN;
} else {
this._value = new BigInteger([0].concat(j), 256);
this._value = new BigInteger([0].concat(j), 256);
}
this._update();
@@ -215,11 +211,10 @@ UInt.prototype.parse_bytes = function (j) {
UInt.prototype.parse_json = UInt.prototype.parse_hex;
UInt.prototype.parse_bn = function (j) {
if (j instanceof sjcl.bn &&
j.bitLength() <= this.constructor.width * 8) {
UInt.prototype.parse_bn = function(j) {
if ((j instanceof sjcl.bn) && j.bitLength() <= this.constructor.width * 8) {
var bytes = sjcl.codec.bytes.fromBits(j.toBits());
this._value = new BigInteger(bytes, 256);
this._value = new BigInteger(bytes, 256);
} else {
this._value = NaN;
}
@@ -229,14 +224,11 @@ UInt.prototype.parse_bn = function (j) {
return this;
};
UInt.prototype.parse_number = function (j) {
UInt.prototype.parse_number = function(j) {
this._value = NaN;
if ("number" === typeof j &&
j === +j &&
j > 0) {
// XXX Better, faster way to get BigInteger from JS int?
this._value = new BigInteger(""+j);
if (typeof j === 'number' && isFinite(j) && j >= 0) {
this._value = new BigInteger(String(j));
}
this._update();
@@ -245,24 +237,33 @@ UInt.prototype.parse_number = function (j) {
};
// Convert from internal form.
UInt.prototype.to_bytes = function () {
if (!(this._value instanceof BigInteger))
UInt.prototype.to_bytes = function() {
if (!(this._value instanceof BigInteger)) {
return null;
}
var bytes = this._value.toByteArray();
bytes = bytes.map(function (b) { return (b+256) % 256; });
bytes = bytes.map(function(b) {
return (b + 256) % 256;
});
var target = this.constructor.width;
// XXX Make sure only trim off leading zeros.
bytes = bytes.slice(-target);
while (bytes.length < target) bytes.unshift(0);
while (bytes.length < target) {
bytes.unshift(0);
}
return bytes;
};
UInt.prototype.to_hex = function () {
if (!(this._value instanceof BigInteger))
UInt.prototype.to_hex = function() {
if (!(this._value instanceof BigInteger)) {
return null;
}
var bytes = this.to_bytes();
return sjcl.codec.hex.fromBits(sjcl.codec.bytes.toBits(bytes)).toUpperCase();
@@ -270,18 +271,20 @@ UInt.prototype.to_hex = function () {
UInt.prototype.to_json = UInt.prototype.to_hex;
UInt.prototype.to_bits = function () {
if (!(this._value instanceof BigInteger))
UInt.prototype.to_bits = function() {
if (!(this._value instanceof BigInteger)) {
return null;
}
var bytes = this.to_bytes();
return sjcl.codec.bytes.toBits(bytes);
};
UInt.prototype.to_bn = function () {
if (!(this._value instanceof BigInteger))
UInt.prototype.to_bn = function() {
if (!(this._value instanceof BigInteger)) {
return null;
}
var bits = this.to_bits();

View File

@@ -1,12 +1,6 @@
var sjcl = require('./utils').sjcl;
var utils = require('./utils');
var config = require('./config');
var extend = require('extend');
var BigInteger = utils.jsbn.BigInteger;
var UInt = require('./uint').UInt,
Base = require('./base').Base;
var utils = require('./utils');
var extend = require('extend');
var UInt = require('./uint').UInt;
//
// UInt128 support
@@ -21,9 +15,9 @@ UInt128.width = 16;
UInt128.prototype = extend({}, UInt.prototype);
UInt128.prototype.constructor = UInt128;
var HEX_ZERO = UInt128.HEX_ZERO = "00000000000000000000000000000000";
var HEX_ONE = UInt128.HEX_ONE = "00000000000000000000000000000000";
var STR_ZERO = UInt128.STR_ZERO = utils.hexToString(HEX_ZERO);
var STR_ONE = UInt128.STR_ONE = utils.hexToString(HEX_ONE);
var HEX_ZERO = UInt128.HEX_ZERO = '00000000000000000000000000000000';
var HEX_ONE = UInt128.HEX_ONE = '00000000000000000000000000000000';
var STR_ZERO = UInt128.STR_ZERO = utils.hexToString(HEX_ZERO);
var STR_ONE = UInt128.STR_ONE = utils.hexToString(HEX_ONE);
exports.UInt128 = UInt128;

View File

@@ -1,4 +1,3 @@
var sjcl = require('./utils').sjcl;
var utils = require('./utils');
var config = require('./config');
var extend = require('extend');
@@ -12,11 +11,10 @@ var Base = require('./base').Base;
// UInt160 support
//
var UInt160 = extend(function () {
var UInt160 = extend(function() {
// Internal form: NaN or BigInteger
this._value = NaN;
this._version_byte = void(0);
this._update();
}, UInt);
@@ -31,20 +29,19 @@ var HEX_ONE = UInt160.HEX_ONE = '0000000000000000000000000000000000000
var STR_ZERO = UInt160.STR_ZERO = utils.hexToString(HEX_ZERO);
var STR_ONE = UInt160.STR_ONE = utils.hexToString(HEX_ONE);
UInt160.prototype.set_version = function (j) {
UInt160.prototype.set_version = function(j) {
this._version_byte = j;
return this;
};
UInt160.prototype.get_version = function () {
UInt160.prototype.get_version = function() {
return this._version_byte;
};
// value = NaN on error.
UInt160.prototype.parse_json = function (j) {
UInt160.prototype.parse_json = function(j) {
// Canonicalize and validate
if (config.accounts && j in config.accounts) {
if (config.accounts && (j in config.accounts)) {
j = config.accounts[j].account;
}
@@ -68,11 +65,11 @@ UInt160.prototype.parse_json = function (j) {
return this;
};
UInt160.prototype.parse_generic = function (j) {
UInt160.prototype.parse_generic = function(j) {
UInt.prototype.parse_generic.call(this, j);
if (isNaN(this._value)) {
if ("string" === typeof j && j[0] === 'r') {
if ((typeof j === 'string') && j[0] === 'r') {
this._value = Base.decode_check(Base.VER_ACCOUNT_ID, j);
}
}
@@ -83,13 +80,14 @@ UInt160.prototype.parse_generic = function (j) {
};
// XXX Json form should allow 0 and 1, C++ doesn't currently allow it.
UInt160.prototype.to_json = function (opts) {
UInt160.prototype.to_json = function(opts) {
opts = opts || {};
if (this._value instanceof BigInteger) {
// If this value has a type, return a Base58 encoded string.
if ("number" === typeof this._version_byte) {
if (typeof this._version_byte === 'number') {
var output = Base.encode_check(this._version_byte, this.to_bytes());
if (opts.gateways && output in opts.gateways) {
output = opts.gateways[output];
}

View File

@@ -1,31 +1,23 @@
var sjcl = require('./utils').sjcl;
var utils = require('./utils');
var config = require('./config');
var extend = require('extend');
var BigInteger = utils.jsbn.BigInteger;
var UInt = require('./uint').UInt,
Base = require('./base').Base;
var utils = require('./utils');
var extend = require('extend');
var UInt = require('./uint').UInt;
//
// UInt256 support
//
var UInt256 = extend(function () {
var UInt256 = extend(function() {
// Internal form: NaN or BigInteger
this._value = NaN;
this._value = NaN;
}, UInt);
UInt256.width = 32;
UInt256.prototype = extend({}, UInt.prototype);
UInt256.prototype.constructor = UInt256;
var HEX_ZERO = UInt256.HEX_ZERO = "00000000000000000000000000000000" +
"00000000000000000000000000000000";
var HEX_ONE = UInt256.HEX_ONE = "00000000000000000000000000000000" +
"00000000000000000000000000000001";
var STR_ZERO = UInt256.STR_ZERO = utils.hexToString(HEX_ZERO);
var STR_ONE = UInt256.STR_ONE = utils.hexToString(HEX_ONE);
var HEX_ZERO = UInt256.HEX_ZERO = '00000000000000000000000000000000' + '00000000000000000000000000000000';
var HEX_ONE = UInt256.HEX_ONE = '00000000000000000000000000000000' + '00000000000000000000000000000001';
var STR_ZERO = UInt256.STR_ZERO = utils.hexToString(HEX_ZERO);
var STR_ONE = UInt256.STR_ONE = utils.hexToString(HEX_ONE);
exports.UInt256 = UInt256;

View File

@@ -1,8 +1,3 @@
Function.prototype.method = function(name, func) {
this.prototype[name] = func;
return this;
};
function filterErr(code, done) {
return function(e) {
done(e.code !== code ? e : void(0));
@@ -20,7 +15,7 @@ function throwErr(done) {
function trace(comment, func) {
return function() {
console.log("%s: %s", trace, arguments.toString);
console.log('%s: %s', trace, arguments.toString);
func(arguments);
};
};
@@ -94,7 +89,7 @@ function chunkString(str, n, leftAlign) {
function assert(assertion, msg) {
if (!assertion) {
throw new Error("Assertion failed" + (msg ? ": "+msg : "."));
throw new Error('Assertion failed' + (msg ? ': ' + msg : '.'));
}
};
@@ -135,7 +130,12 @@ function fromTimestamp(rpepoch) {
rpepoch = rpepoch.getTime();
}
return Math.round(rpepoch/1000) - 0x386D4380;
return Math.round(rpepoch / 1000) - 0x386D4380;
};
exports.time = {
fromRipple: toTimestamp,
toRipple: fromTimestamp
};
exports.trace = trace;

View File

@@ -0,0 +1,607 @@
var async = require('async');
var blobClient = require('./blob').BlobClient;
var AuthInfo = require('./authinfo').AuthInfo;
var crypt = require('./crypt').Crypt;
var log = require('./log').sub('vault');
function VaultClient(opts) {
var self = this;
if (!opts) {
opts = { };
}
if (typeof opts === 'string') {
opts = { domain: opts };
}
this.domain = opts.domain || 'ripple.com';
this.infos = { };
};
/**
* getAuthInfo
* gets auth info for a username. returns authinfo
* even if user does not exists (with exist set to false)
* @param {string} username
* @param {function} callback
*/
VaultClient.prototype.getAuthInfo = function (username, callback) {
AuthInfo.get(this.domain, username, function(err, authInfo) {
if (err) {
return callback(err);
}
if (authInfo.version !== 3) {
return callback(new Error('This wallet is incompatible with this version of the vault-client.'));
}
if (!authInfo.pakdf) {
return callback(new Error('No settings for PAKDF in auth packet.'));
}
if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
}
callback(null, authInfo);
});
};
/**
* _deriveLoginKeys
* method designed for asnyc waterfall
*/
VaultClient.prototype._deriveLoginKeys = function (authInfo, password, callback) {
//derive login keys
crypt.derive(authInfo.pakdf, 'login', authInfo.username.toLowerCase(), password, function(err, keys) {
if (err) {
callback(err);
} else {
callback(null, authInfo, password, keys);
}
});
};
/**
* _deriveUnlockKey
* method designed for asnyc waterfall
*/
VaultClient.prototype._deriveUnlockKey = function (authInfo, password, keys, callback) {
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', authInfo.username.toLowerCase(), password, function(err, unlock) {
if (err) {
log.error('derive:', err);
return callback(err);
}
if (!keys) {
keys = { };
}
keys.unlock = unlock.unlock;
callback(null, authInfo, keys);
});
};
/**
* Get a ripple name from a given account address, if it has one
* @param {string} address - Account address to query
* @param {string} url - Url of blob vault
*/
VaultClient.prototype.getRippleName = function(address, url, callback) {
//use the url from previously retrieved authInfo, if necessary
if (!url) {
callback(new Error('Blob vault URL is required'));
} else {
blobClient.getRippleName(url, address, callback);
}
};
/**
* Check blobvault for existance of username
*
* @param {string} username
* @param {function} fn - Callback function
*/
VaultClient.prototype.exists = function(username, callback) {
AuthInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) {
callback(err);
} else {
callback(null, !!authInfo.exists);
}
});
};
/**
* Authenticate and retrieve a decrypted blob using a ripple name and password
*
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.login = function(username, password, callback) {
var self = this;
var steps = [
getAuthInfo,
self._deriveLoginKeys,
getBlob
];
async.waterfall(steps, callback);
function getAuthInfo(callback) {
self.getAuthInfo(username, function(err, authInfo){
if (authInfo && !authInfo.exists) {
return callback(new Error('User does not exist.'));
}
return callback (err, authInfo, password);
});
}
function getBlob(authInfo, password, keys, callback) {
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) {
if (err) {
return callback(err);
}
//save for relogin
self.infos[keys.id] = authInfo;
//migrate missing fields
if (blob.missing_fields) {
if (blob.missing_fields.encrypted_blobdecrypt_key) {
log.info('migration: saving encrypted blob decrypt key');
authInfo.blob = blob;
//get the key to unlock the secret, then update the blob keys
self._deriveUnlockKey(authInfo, password, keys, updateKeys);
}
}
callback(null, {
blob : blob,
username : authInfo.username,
verified : authInfo.emailVerified
});
});
};
function updateKeys (err, params, keys) {
if (err || !keys.unlock) {
return; //unable to unlock
}
var secret;
try {
secret = crypt.decrypt(keys.unlock, params.blob.encrypted_secret);
} catch (error) {
return log.error('decrypt:', error);
}
options = {
username : params.username,
blob : params.blob,
masterkey : secret,
keys : keys
};
blobClient.updateKeys(options, function(err, resp){
if (err) {
log.error('updateKeys:', err);
}
});
}
};
/**
* Retreive and decrypt blob using a blob url, id and crypt derived previously.
*
* @param {string} url - Blob vault url
* @param {string} id - Blob id from previously retreived blob
* @param {string} key - Blob decryption key
* @param {function} fn - Callback function
*/
VaultClient.prototype.relogin = function(url, id, key, callback) {
//use the url from previously retrieved authInfo, if necessary
if (!url && this.infos[id]) {
url = this.infos[id].blobvault;
}
if (!url) {
return callback(new Error('Blob vault URL is required'));
}
blobClient.get(url, id, key, function(err, blob) {
if (err) {
callback(err);
} else {
callback (null, { blob: blob });
}
});
};
/**
* Decrypt the secret key using a username and password
*
* @param {string} username
* @param {string} password
* @param {string} encryptSecret
* @param {function} fn - Callback function
*/
VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) {
var self = this;
var steps = [
getAuthInfo,
self._deriveUnlockKey,
unlockSecret
];
async.waterfall(steps, fn);
function getAuthInfo(callback) {
self.getAuthInfo(username, function(err, authInfo){
if (authInfo && !authInfo.exists) {
return callback(new Error('User does not exist.'));
}
return callback (err, authInfo, password, {});
});
}
function unlockSecret (authinfo, keys, callback) {
var secret;
try {
secret = crypt.decrypt(keys.unlock, encryptSecret);
} catch (error) {
return callback(error);
}
callback(null, {
keys : keys,
secret : secret
});
}
};
/**
* Retrieve the decrypted blob and secret key in one step using
* the username and password
*
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
var self = this;
var steps = [
login,
deriveUnlockKey,
unlockSecret
];
async.waterfall(steps, fn);
function login (callback) {
self.login(username, password, function(err, resp) {
if (err) {
return callback(err);
}
if (!resp.blob || !resp.blob.encrypted_secret) {
return callback(new Error('Unable to retrieve blob and secret.'));
}
if (!resp.blob.id || !resp.blob.key) {
return callback(new Error('Unable to retrieve keys.'));
}
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) {
return callback(new Error('Unable to find authInfo'));
}
callback(null, authInfo, password, resp.blob);
});
};
function deriveUnlockKey (authInfo, password, blob, callback) {
self._deriveUnlockKey(authInfo, password, null, function(err, authInfo, keys){
callback(err, keys.unlock, authInfo, blob);
});
};
function unlockSecret (unlock, authInfo, blob, callback) {
var secret;
try {
secret = crypt.decrypt(unlock, blob.encrypted_secret);
} catch (error) {
return callback(error);
}
callback(null, {
blob : blob,
unlock : unlock,
secret : secret,
username : authInfo.username,
verified : authInfo.emailVerified
});
};
};
/**
* Verify an email address for an existing user
*
* @param {string} username
* @param {string} token - Verification token
* @param {function} fn - Callback function
*/
VaultClient.prototype.verify = function(username, token, callback) {
var self = this;
self.getAuthInfo(username, function (err, authInfo){
if (err) {
return callback(err);
}
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, callback);
});
};
/**
* resendEmail
* send a new verification email
* @param {object} options
* @param {string} options.id
* @param {string} options.username
* @param {string} options.account_id
* @param {string} options.email
* @param {string} options.activateLink
* @param {function} fn - Callback
*/
VaultClient.prototype.resendEmail = function (options, fn) {
blobClient.resendEmail(options, fn);
};
/**
* deleteBlob
* @param {object} options
* @param {string} options.url
* @param {string} options.username
* @param {string} options.blob_id
* @param {string} options.account_id
* @param {string} options.masterkey
*/
VaultClient.prototype.deleteBlob = function (options, fn) {
blobClient.deleteBlob(options, fn);
};
/**
* updateProfile
* update information stored outside the blob
* @param {object}
* @param {string} options.url
* @param {string} options.username
* @param {string} options.auth_secret
* @param {srring} options.blob_id
* @param {object} options.profile
* @param {string} options.profile.phone - optional
* @param {string} options.profile.country - optional
* @param {string} options.profile.region - optional
* @param {string} options.profile.city - optional
*/
VaultClient.prototype.updateProfile = function (options, fn) {
blobClient.updateProfile(options, fn);
};
/**
* recoverBlob
* recover blob with account secret
* @param {object} options
* @param {string} options.url
* @param {string} options.username
* @param {string} options.masterkey
* @param {function}
*/
VaultClient.prototype.recoverBlob = function (options, fn) {
blobClient.recoverBlob(options, fn);
};
/*
* changePassword
* @param {object} options
* @param {string} options.username
* @param {string} options.password
* @param {string} options.masterkey
* @param {object} options.blob
*/
VaultClient.prototype.changePassword = function (options, fn) {
var self = this;
var password = String(options.password).trim();
var steps = [
getAuthInfo,
self._deriveLoginKeys,
self._deriveUnlockKey,
changePassword
];
async.waterfall(steps, fn);
function getAuthInfo(callback) {
self.getAuthInfo(options.username, function(err, authInfo) {
return callback (err, authInfo, password);
});
};
function changePassword (authInfo, keys, callback) {
options.keys = keys;
blobClient.updateKeys(options, callback);
};
};
/**
* rename
* rename a ripple account
* @param {object} options
* @param {string} options.username
* @param {string} options.new_username
* @param {string} options.password
* @param {string} options.masterkey
* @param {object} options.blob
* @param {function} fn
*/
VaultClient.prototype.rename = function (options, fn) {
var self = this;
var new_username = String(options.new_username).trim();
var password = String(options.password).trim();
var steps = [
getAuthInfo,
self._deriveLoginKeys,
self._deriveUnlockKey,
renameBlob
];
async.waterfall(steps, fn);
function getAuthInfo(callback) {
self.getAuthInfo(new_username, function(err, authInfo){
if (authInfo && authInfo.exists) {
return callback(new Error('username already taken.'));
} else {
authInfo.username = new_username;
}
return callback (err, authInfo, password);
});
};
function renameBlob (authInfo, keys, callback) {
options.keys = keys;
blobClient.rename(options, callback);
};
};
/**
* Register a new user and save to the blob vault
*
* @param {object} options
* @param {string} options.username
* @param {string} options.password
* @param {string} options.masterkey //optional, will create if absent
* @param {string} options.email
* @param {string} options.activateLink
* @param {object} options.oldUserBlob //optional
* @param {function} fn
*/
VaultClient.prototype.register = function(options, fn) {
var self = this;
var username = String(options.username).trim();
var password = String(options.password).trim();
var result = self.validateUsername(username);
if (!result.valid) {
return fn(new Error('invalid username.'));
}
var steps = [
getAuthInfo,
self._deriveLoginKeys,
self._deriveUnlockKey,
create
];
async.waterfall(steps, fn);
function getAuthInfo(callback) {
self.getAuthInfo(username, function(err, authInfo){
return callback (err, authInfo, password);
});
};
function create(authInfo, keys, callback) {
var params = {
url : authInfo.blobvault,
id : keys.id,
crypt : keys.crypt,
unlock : keys.unlock,
username : username,
email : options.email,
masterkey : options.masterkey || crypt.createMaster(),
activateLink : options.activateLink,
oldUserBlob : options.oldUserBlob,
domain : options.domain
};
blobClient.create(params, function(err, blob) {
if (err) {
callback(err);
} else {
callback(null, {
blob : blob,
username : username
});
}
});
};
};
VaultClient.prototype.validateUsername = function (username) {
username = String(username).trim();
var result = {
valid : false,
reason : ''
};
if (username.length < 2) {
result.reason = 'tooshort';
} else if (username.length > 20) {
result.reason = 'toolong';
} else if (!/^[a-zA-Z0-9\-]+$/.exec(username)) {
result.reason = 'charset';
} else if (/^-/.exec(username)) {
result.reason = 'starthyphen';
} else if (/-$/.exec(username)) {
result.reason = 'endhyphen';
} else if (/--/.exec(username)) {
result.reason = 'multhyphen';
} else {
result.valid = true;
}
return result;
};
exports.VaultClient = VaultClient;

View File

@@ -0,0 +1,83 @@
/**
* Check that the point is valid based on the method described in
* SEC 1: Elliptic Curve Cryptography, section 3.2.2.1:
* Elliptic Curve Public Key Validation Primitive
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isValidPoint = function() {
var self = this;
var field_modulus = self.curve.field.modulus;
if (self.isIdentity) {
return false;
}
// Check that coordinatres are in bounds
// Return false if x < 1 or x > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.x)) &&
!self.x.equals(1)) ||
(self.x.greaterEquals(field_modulus.sub(1))) &&
!self.x.equals(1)) {
return false;
}
// Return false if y < 1 or y > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.y)) &&
!self.y.equals(1)) ||
(self.y.greaterEquals(field_modulus.sub(1))) &&
!self.y.equals(1)) {
return false;
}
if (!self.isOnCurve()) {
return false;
}
// TODO check to make sure point is a scalar multiple of base_point
return true;
};
/**
* Check that the point is on the curve
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isOnCurve = function() {
var self = this;
var field_order = self.curve.r;
var component_a = self.curve.a;
var component_b = self.curve.b;
var field_modulus = self.curve.field.modulus;
var left_hand_side = self.y.mul(self.y).mod(field_modulus);
var right_hand_side = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus);
return left_hand_side.equals(right_hand_side);
};
sjcl.ecc.point.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() +
')';
};
sjcl.ecc.pointJac.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() + ', ' +
this.z.toString() +
')';
};

View File

@@ -0,0 +1,17 @@
sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength();
var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l));
// For a canonical signature we want the lower of two possible values for s
// 0 < s <= n/2
if (!R.copy().halveM().greaterEquals(s)) {
s = R.sub(s);
}
return w.concat(r.toBits(l), s.toBits(l));
};

View File

@@ -0,0 +1,306 @@
/**
* This module uses the public key recovery method
* described in SEC 1: Elliptic Curve Cryptography,
* section 4.1.6, "Public Key Recovery Operation".
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* Implementation based on:
* https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js
*/
// Defined here so that this value only needs to be calculated once
var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR;
/**
* Sign the given hash such that the public key, prepending an extra byte
* so that the public key will be recoverable from the signature
*
* @param {bitArray} hash
* @param {Number} paranoia
* @returns {bitArray} Signature formatted as bitArray
*/
sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) {
var self = this;
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
// Sign hash with standard, canonicalized method
var standard_signature = self.sign(hash_bits, paranoia, k_for_testing);
var canonical_signature = self.canonicalizeSignature(standard_signature);
// Extract r and s signature components from canonical signature
var r_and_s = getRandSFromSignature(self._curve, canonical_signature);
// Rederive public key
var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get()));
// Determine recovery factor based on which possible value
// returns the correct public key
var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key);
// Prepend recovery_factor to signature and encode in DER
// The value_to_prepend should be 4 bytes total
var value_to_prepend = recovery_factor + 27;
var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature);
// Return value in bits
return final_signature_bits;
};
/**
* Recover the public key from a signature created with the
* signWithRecoverablePublicKey method in this module
*
* @static
*
* @param {bitArray} hash
* @param {bitArray} signature
* @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve
* @returns {sjcl.ecc.ecdsa.publicKey} Public key
*/
sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) {
if (!signature || signature instanceof sjcl.ecc.curve) {
throw new sjcl.exception.invalid('must supply hash and signature to recover public key');
}
if (!curve) {
curve = sjcl.ecc.curves['c256'];
}
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
var signature_bits;
if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') {
signature_bits = signature;
} else {
throw new sjcl.exception.invalid('signature. Must be a bitArray');
}
// Extract recovery_factor from first 4 bytes
var recovery_factor = signature_bits[0] - 27;
if (recovery_factor < 0 || recovery_factor > 3) {
throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' +
'that prepends the recovery factor in order to recover the public key');
}
// Separate r and s values
var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1));
var signature_r = r_and_s.r;
var signature_s = r_and_s.s;
// Recover public key using recovery_factor
var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor);
var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point);
return recovered_public_key;
};
/**
* Retrieve the r and s components of a signature
*
* @param {sjcl.ecc.curve} curve
* @param {bitArray} signature
* @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn
*/
function getRandSFromSignature(curve, signature) {
var r_length = curve.r.bitLength();
return {
r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)),
s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature)))
};
};
/**
* Determine the recovery factor by trying all four
* possibilities and figuring out which results in the
* correct public key
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {sjcl.ecc.point} original_public_key_point
* @returns {Number, 0-3} Recovery factor
*/
function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) {
var original_public_key_point_bits = original_public_key_point.toBits();
// TODO: verify that it is possible for the recovery_factor to be 2 or 3,
// we may only need 1 bit because the canonical signature might remove the
// possibility of us needing to "use the second candidate key"
for (var possible_factor = 0; possible_factor < 4; possible_factor++) {
var resulting_public_key_point;
try {
resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor);
} catch (err) {
// console.log(err, err.stack);
continue;
}
if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) {
return possible_factor;
}
}
throw new sjcl.exception.bug('unable to calculate recovery factor from signature');
};
/**
* Recover the public key from the signature.
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {Number, 0-3} recovery_factor
* @returns {sjcl.point} Public key corresponding to signature
*/
function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) {
var field_order = curve.r;
var field_modulus = curve.field.modulus;
// Reduce the recovery_factor to the two bits used
recovery_factor = recovery_factor & 3;
// The less significant bit specifies whether the y coordinate
// of the compressed point is even or not.
var compressed_point_y_coord_is_even = recovery_factor & 1;
// The more significant bit specifies whether we should use the
// first or second candidate key.
var use_second_candidate_key = recovery_factor >> 1;
// Calculate (field_order + 1) / 4
if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) {
FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4);
}
// In the paper they write "1. For j from 0 to h do the following..."
// That is not necessary here because we are given the recovery_factor
// step 1.1 Let x = r + jn
// Here "j" is either 0 or 1
var x;
if (use_second_candidate_key) {
x = signature_r.add(field_order);
} else {
x = signature_r;
}
// step 1.2 and 1.3 convert x to an elliptic curve point
// Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion
var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus);
var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus);
// If beta is even but y isn't or
// if beta is odd and y is even
// then subtract beta from the field_modulus
var y;
var beta_is_even = beta.mod(2).equals(0);
if (beta_is_even && !compressed_point_y_coord_is_even ||
!beta_is_even && compressed_point_y_coord_is_even) {
y = beta;
} else {
y = field_modulus.sub(beta);
}
// generated_point_R is the point generated from x and y
var generated_point_R = new sjcl.ecc.point(curve, x, y);
// step 1.4 check that R is valid and R x field_order !== infinity
// TODO: add check for R x field_order === infinity
if (!generated_point_R.isValidPoint()) {
throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key');
}
// step 1.5 Compute e from M
var message_e = sjcl.bn.fromBits(hash_bits);
var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order);
// step 1.6 Compute Q = r^-1 (sR - eG)
// console.log('r: ', signature_r);
var signature_r_inv = signature_r.inverseMod(field_order);
var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv);
// Validate public key point
if (!public_key_point.isValidPoint()) {
throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key');
}
// Verify that this public key matches the signature
if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) {
throw new sjcl.exception.corrupt('cannot recover public key');
}
return public_key_point;
};
/**
* Verify a signature given the raw components
* using method defined in section 4.1.5:
* "Alternative Verifying Operation"
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} e
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {sjcl.ecc.point} public_key_point
* @returns {Boolean}
*/
function verify_raw(curve, e, r, s, public_key_point) {
var field_order = curve.r;
// Return false if r is out of bounds
if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Return false if s is out of bounds
if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Check that r = (u1 + u2)G
// u1 = e x s^-1 (mod field_order)
// u2 = r x s^-1 (mod field_order)
var s_mod_inverse_field_order = s.inverseMod(field_order);
var u1 = e.mul(s_mod_inverse_field_order).mod(field_order);
var u2 = r.mul(s_mod_inverse_field_order).mod(field_order);
var point_computed = curve.G.mult2(u1, u2, public_key_point);
return r.equals(point_computed.x.mod(field_order));
};

View File

@@ -62,7 +62,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () {
var f = e.square();
var x = f.sub(d.copy().doubleM());
var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM());
var z = this.y.mul(this.z).doubleM();
var z = this.z.mul(this.y).doubleM();
return new sjcl.ecc.pointJac(this.curve, x, y, z);
};

View File

@@ -1,30 +1,38 @@
sjcl.ecc.ecdsa.secretKey.prototype = {
sign: function(hash, paranoia) {
var R = this._curve.r,
l = R.bitLength(),
k = sjcl.bn.random(R.sub(1), paranoia).add(1),
r = this._curve.G.mult(k).x.mod(R),
s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) {
var R = this._curve.r,
l = R.bitLength();
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
// k_for_testing should ONLY BE SPECIFIED FOR TESTING
// specifying it will make the signature INSECURE
var k;
if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') {
k = k_for_testing;
} else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) {
k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing));
} else {
// This is the only option that should be used in production
k = sjcl.bn.random(R.sub(1), paranoia).add(1);
}
var r = this._curve.G.mult(k).x.mod(R);
var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
};
sjcl.ecc.ecdsa.publicKey.prototype = {
verify: function(hash, rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength(),
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
sInv = s.inverseMod(R),
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
hA = r.mul(sInv).mod(R),
r2 = this._curve.G.mult2(hG, hA, this._point).x;
sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) {
var w = sjcl.bitArray,
R = this._curve.r,
l = R.bitLength(),
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
sInv = s.inverseMod(R),
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
hA = r.mul(sInv).mod(R),
r2 = this._curve.G.mult2(hG, hA, this._point).x;
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
throw (new sjcl.exception.corrupt("signature didn't check out"));
}
return true;
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
throw (new sjcl.exception.corrupt("signature didn't check out"));
}
return true;
};

View File

@@ -11,8 +11,16 @@ sjcl.ecc.point = function(curve,x,y) {
if (x === undefined) {
this.isIdentity = true;
} else {
if (x instanceof sjcl.bn) {
x = new curve.field(x);
}
if (y instanceof sjcl.bn) {
y = new curve.field(y);
}
this.x = x;
this.y = y;
this.isIdentity = false;
}
this.curve = curve;

181
test/account-test.js Normal file
View File

@@ -0,0 +1,181 @@
var assert = require('assert');
var Account = require('../src/js/ripple/account').Account;
describe('Account', function(){
describe('._publicKeyToAddress()', function(){
it('should throw an error if the key is invalid', function(){
try {
Account._publicKeyToAddress('not a real key');
} catch (e) {
assert(e);
}
});
it('should return unchanged a valid UINT160', function(){
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'));
});
it('should parse a hex-encoded public key as a UINT160', function(){
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332'));
assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === Account._publicKeyToAddress('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23'));
assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === Account._publicKeyToAddress('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7'));
assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === Account._publicKeyToAddress('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F'));
});
});
describe('.publicKeyIsActive()', function(){
it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: 65536,
LedgerEntryType: 'AccountRoot'
}});
}
}
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){
assert(err === null);
assert(is_valid === true);
});
});
it('should respond false if the public key corresponds to the account address and the master key IS disabled', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot'
}});
}
}
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){
assert(err === null);
assert(is_valid === false);
});
});
it('should respond true if the public key corresponds to the regular key', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
account.publicKeyIsActive('02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){
assert(err === null);
assert(is_valid === true);
});
});
it('should respond false if the public key does not correspond to an active public key for the account', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
account.publicKeyIsActive('032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){
assert(err === null);
assert(is_valid === false);
});
});
it('should respond false if the public key is invalid', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
account.publicKeyIsActive('not a real public key', function(err, is_valid){
assert(err);
});
});
it('should assume the master key is valid for unfunded accounts', function(){
var account = new Account({
on: function(){},
request_account_info: function(address, callback) {
if (address === 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ') {
callback({ error: 'remoteError',
error_message: 'Remote reported an error.',
remote:
{ account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ',
error: 'actNotFound',
error_code: 15,
error_message: 'Account not found.',
id: 3,
ledger_current_index: 6391106,
request:
{ account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ',
command: 'account_info',
id: 3,
ident: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' },
status: 'error',
type: 'response' },
result: 'remoteError',
engine_result: 'remoteError',
result_message: 'Remote reported an error.',
engine_result_message: 'Remote reported an error.',
message: 'Remote reported an error.'
});
}
}
}, 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ');
account.publicKeyIsActive('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){
assert(!err);
assert(is_valid);
});
});
});
});

View File

@@ -16,10 +16,41 @@ describe('Amount', function() {
assert(Amount.from_json('1').is_positive());
});
});
describe('from_human', function() {
it('1 XRP', function() {
assert.strictEqual(Amount.from_human("1 XRP").to_text_full(), '1/XRP');
});
it('0.1 XRP', function() {
assert.strictEqual(Amount.from_human("0.1 XRP").to_text_full(), '0.1/XRP');
});
it('0.1 USD', function() {
assert.strictEqual(Amount.from_human("0.1 USD").to_text_full(), '0.1/USD/NaN');
});
});
describe('from_json', function() {
it('1 XRP', function() {
assert.strictEqual(Amount.from_json("1/XRP").to_text_full(), "1/XRP/NaN");
});
});
describe('from_number', function() {
it('Number 1', function() {
assert.strictEqual(Amount.from_number(1).to_text_full(), '1/1/rrrrrrrrrrrrrrrrrrrrBZbvji');
});
it('Number 2', function() {
assert.strictEqual(Amount.from_number(2).to_text_full(), '2/1/rrrrrrrrrrrrrrrrrrrrBZbvji');
});
it('Multiply 2 "1" with 3 "1", by product_human', function () {
assert.strictEqual(Amount.from_number(2).product_human(Amount.from_number(3)).to_text_full(), '6/1/rrrrrrrrrrrrrrrrrrrrBZbvji');
});
it('Multiply 3 USD with 3 "1"', function () {
assert.strictEqual(Amount.from_json('3/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').multiply(Amount.from_number(3)).to_text_full(), '9/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply -1 "1" with 3 USD', function () {
assert.strictEqual(Amount.from_number(-1).multiply(Amount.from_json('3/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-3/1/rrrrrrrrrrrrrrrrrrrrBZbvji');
});
it('Multiply -1 "1" with 3 USD, by product_human', function () {
assert.strictEqual(Amount.from_number(-1).product_human(Amount.from_json('3/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-3/1/rrrrrrrrrrrrrrrrrrrrBZbvji');
});
});
describe('text_full_rewrite', function() {
it('Number 1', function() {
@@ -128,6 +159,12 @@ describe('Amount', function() {
it('Parse -0.0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('-0.0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').to_text_full());
});
it('Parse 0.0/111/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
assert.strictEqual('0/111/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/111/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').to_text_full());
});
it('Parse 0.0/12D/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
assert.strictEqual('0/XRP/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/12D/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').to_text_full());
});
});
describe('Amount operations', function() {
it('Negate native 123', function () {
@@ -431,4 +468,130 @@ describe('Amount', function() {
assert.strictEqual(a.not_equals_why(b), 'Native mismatch.');
});
});
describe('product_human', function() {
it('Multiply 0 XRP with 0 XRP', function () {
assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0')).to_text_full());
});
it('Multiply 0 USD with 0 XRP', function () {
assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full());
});
it('Multiply 0 XRP with 0 USD', function () {
assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply 1 XRP with 0 XRP', function () {
assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0')).to_text_full());
});
it('Multiply 1 USD with 0 XRP', function () {
assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full());
});
it('Multiply 1 XRP with 0 USD', function () {
assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply 0 XRP with 1 XRP', function () {
assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1')).to_text_full());
});
it('Multiply 0 USD with 1 XRP', function () {
assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1')).to_text_full());
});
it('Multiply 0 XRP with 1 USD', function () {
assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply XRP with USD', function () {
assert.equal('0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply XRP with USD', function () {
assert.strictEqual('0.2/XRP', Amount.from_json('20000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply XRP with USD', function () {
assert.strictEqual('20/XRP', Amount.from_json('2000000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply XRP with USD, neg', function () {
assert.strictEqual('-0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('-10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply XRP with USD, neg, frac', function () {
assert.strictEqual('-0.222/XRP', Amount.from_json('-6000').product_human(Amount.from_json('37/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply USD with USD', function () {
assert.strictEqual('20000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply USD with USD', function () {
assert.strictEqual('200000000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('100000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply EUR with USD, result < 1', function () {
assert.strictEqual('100000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full());
});
it('Multiply EUR with USD, neg', function () {
assert.strictEqual(Amount.from_json('-24000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-48000000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply EUR with USD, neg, <1', function () {
assert.strictEqual(Amount.from_json('0.1/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('-1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply EUR with XRP, factor < 1', function () {
assert.strictEqual(Amount.from_json('0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply EUR with XRP, neg', function () {
assert.strictEqual(Amount.from_json('-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('5')).to_text_full(), '-0.0005/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply EUR with XRP, neg, <1', function () {
assert.strictEqual(Amount.from_json('-0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '-0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Multiply XRP with XRP', function () {
assert.strictEqual(Amount.from_json('10000000').product_human(Amount.from_json('10')).to_text_full(), '0.0001/XRP');
});
it('Multiply USD with XAU (dem)', function () {
assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '19900.00316303882/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
});
describe('ratio_human', function() {
it('Divide USD by XAU (dem)', function () {
assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
});
describe('_invert', function() {
it('Invert 1', function () {
assert.strictEqual(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Invert 20', function () {
assert.strictEqual(Amount.from_json('20/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '0.05/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
it('Invert 0.02', function () {
assert.strictEqual(Amount.from_json('0.02/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '50/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});
});
describe('from_quality', function() {
it('BTC/XRP', function () {
assert.strictEqual(Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'BTC'}).to_text_full(), '44,970/XRP');
});
it('BTC/XRP inverse', function () {
assert.strictEqual(Amount.from_quality('37AAC93D336021AE94310D0430FFA090F7137C97D473488C4A0918D0DEF8624E', 'XRP', NaN, {inverse: true, base_currency: 'BTC'}).to_text_full(), '39,053.954453/XRP');
});
it('XRP/USD', function () {
assert.strictEqual(Amount.from_quality('DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4D05DCAA8FE12000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'XRP'}).to_text_full(), '0.0165/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
});
it('XRP/USD inverse', function () {
assert.strictEqual(Amount.from_quality('4627DFFCFF8B5A265EDBD8AE8C14A52325DBFEDAF4F5C32E5C22A840E27DCA9B', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'XRP'}).to_text_full(), '0.010251/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
});
it('BTC/USD', function () {
assert.strictEqual(Amount.from_quality('6EAB7C172DEFA430DBFAD120FDC373B5F5AF8B191649EC9858038D7EA4C68000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'BTC'}).to_text_full(), '1000/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
});
it('BTC/USD inverse', function () {
assert.strictEqual(Amount.from_quality('20294C923E80A51B487EB9547B3835FD483748B170D2D0A455071AFD498D0000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'BTC'}).to_text_full(), '0.5/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
});
it('XAU(dem)/XRP', function () {
assert.strictEqual(Amount.from_quality('587322CCBDE0ABD01704769A73A077C32FB39057D813D4165F1FF973CAF997EF', 'XRP', NaN, {base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,452.246928/XRP');
});
it('XAU(dem)/XRP inverse', function () {
assert.strictEqual(Amount.from_quality('F72C7A9EAE4A45ED1FB547AD037D07B9B965C6E662BEBAFA4A03F2A976804235', 'XRP', NaN, {inverse: true, base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,442.196677/XRP');
});
it('USD/XAU(dem)', function () {
assert.strictEqual(Amount.from_quality('4743E58E44974B325D42FD2BB683A6E36950F350EE46DD3A521B644B99782F5F', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007710100231303007/XAU (-0.5%pa)/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN');
});
it('USD/XAU(dem) inverse', function () {
assert.strictEqual(Amount.from_quality('CDFD3AFB2F8C5DBEF75B081F7C957FF5509563266F28F36C5704A0FB0BAD8800', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {inverse: true, base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007675186123263489/XAU (-0.5%pa)/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN');
});
});
});

View File

@@ -10,9 +10,9 @@ describe('Currency', function() {
it('json_rewrite("NaN") == "XRP"', function() {
assert.strictEqual('XRP', currency.json_rewrite(NaN));
});
it('json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000") == "015841551A748AD2C1F76FF6ECB0CCCD00000000"', function() {
it('json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000") == "XAU (-0.5%pa)"', function() {
assert.strictEqual(currency.json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000"),
'015841551A748AD2C1F76FF6ECB0CCCD00000000');
"XAU (-0.5%pa)");
});
});
describe('from_json', function() {
@@ -27,7 +27,60 @@ describe('Currency', function() {
assert(r.is_native());
assert.strictEqual('XRP', r.to_json());
});
it('from_json("111").to_human()', function() {
var r = currency.from_json("111");
assert(r.is_valid());
assert.strictEqual('111', r.to_json());
});
it('from_json("1D2").to_human()', function() {
var r = currency.from_json("1D2");
assert(!r.is_valid());
assert.strictEqual('XRP', r.to_json());
});
});
describe('from_human', function() {
it('From human "USD - Gold (-25%pa)"', function() {
var cur = currency.from_human('USD - Gold (-25%pa)');
assert.strictEqual(cur.to_json(), 'USD (-25%pa)');
assert.strictEqual(cur.to_hex(), '0155534400000000C19A22BC51297F0B00000000');
assert.strictEqual(cur.to_json(), cur.to_human());
});
it('From human "EUR (-0.5%pa)', function() {
var cur = currency.from_human('EUR (-0.5%pa)');
assert.strictEqual(cur.to_json(), 'EUR (-0.5%pa)');
});
it('From human "EUR (0.5361%pa)", test decimals', function() {
var cur = currency.from_human('EUR (0.5361%pa)');
assert.strictEqual(cur.to_json(), 'EUR (0.54%pa)');
assert.strictEqual(cur.to_json({decimals:4}), 'EUR (0.5361%pa)');
assert.strictEqual(cur.get_interest_percentage_at(undefined, 4), 0.5361);
});
it('From human "EUR - Euro (0.5361%pa)", test decimals and full_name', function() {
var cur = currency.from_human('EUR (0.5361%pa)');
assert.strictEqual(cur.to_json(), 'EUR (0.54%pa)');
assert.strictEqual(cur.to_json({decimals:4, full_name:'Euro'}), 'EUR - Euro (0.5361%pa)');
assert.strictEqual(cur.get_interest_percentage_at(undefined, 4), 0.5361);
});
it('From human "TYX - 30-Year Treasuries (1.5%pa)"', function() {
var cur = currency.from_human('TYX - 30-Year Treasuries (1.5%pa)');
assert.strictEqual(cur.to_json(), 'TYX (1.5%pa)');
});
it('From human "TYX - 30-Year Treasuries"', function() {
var cur = currency.from_human('TYX - 30-Year Treasuries');
assert.strictEqual(cur.to_json(), 'TYX');
});
it('From human "INR - Indian Rupees (-0.5%)"', function() {
var cur = currency.from_human('INR - Indian Rupees (-0.5%pa)');
assert.strictEqual(cur.to_json(), 'INR (-0.5%pa)');
});
it('From human "INR - 30 Indian Rupees"', function() {
var cur = currency.from_human('INR - 30 Indian Rupees');
assert.strictEqual(cur.to_json(), 'INR');
});
});
describe('to_human', function() {
it('"USD".to_human() == "USD"', function() {
assert.strictEqual('USD', currency.from_json('USD').to_human());
@@ -37,12 +90,30 @@ describe('Currency', function() {
});
it('"015841551A748AD2C1F76FF6ECB0CCCD00000000") == "015841551A748AD2C1F76FF6ECB0CCCD00000000"', function() {
assert.strictEqual(currency.from_json("015841551A748AD2C1F76FF6ECB0CCCD00000000").to_human(),
'XAU');
'XAU (-0.5%pa)');
});
it('to_human with full_name "USD - US Dollar"', function() {
assert.strictEqual('USD - US Dollar', currency.from_json('USD').to_human({full_name:'US Dollar'}));
});
it('to_human with full_name "XRP - Ripples"', function() {
assert.strictEqual('XRP - Ripples', currency.from_json('XRP').to_human({full_name:'Ripples'}));
});
});
describe('from_hex', function() {
it('"015841551A748AD2C1F76FF6ECB0CCCD00000000" === "XAU (-0.5%pa)"', function() {
var cur = currency.from_hex('015841551A748AD2C1F76FF6ECB0CCCD00000000');
assert.strictEqual(cur.to_json(), 'XAU (-0.5%pa)');
assert.strictEqual(cur.to_hex(), '015841551A748AD2C1F76FF6ECB0CCCD00000000');
assert.strictEqual(cur.to_json(), cur.to_human());
});
});
describe('parse_json(currency obj)', function() {
assert.strictEqual('USD', new currency().parse_json(currency.from_json('USD')).to_json());
assert.strictEqual('USD (0.5%pa)', new currency().parse_json(currency.from_json('USD (0.5%pa)')).to_json());
});
describe('is_valid', function() {
it('Currency.is_valid("XRP")', function() {
assert(currency.is_valid('XRP'));
@@ -101,4 +172,15 @@ describe('Currency', function() {
assert.equal(Math.E, cur.get_interest_at(443845330 - 6291418827.05));
});
});
describe('get_iso', function() {
it('should get "XRP" iso_code', function() {
assert.strictEqual('XRP', currency.from_json('XRP').get_iso());
});
it('should get iso_code', function() {
assert.strictEqual('USD', currency.from_json('USD - US Dollar').get_iso());
});
it('should get iso_code', function() {
assert.strictEqual('USD', currency.from_json('USD (0.5%pa)').get_iso());
});
});
});

1
test/fixtures/ledger-full-38129.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/fixtures/ledger-full-40000.json vendored Normal file

File diff suppressed because one or more lines are too long

88
test/ledger-test.js Normal file
View File

@@ -0,0 +1,88 @@
var assert = require('assert');
var fs = require('fs');
var utils = require('./testutils');
var Ledger = utils.load_module('ledger').Ledger;
var config = require('./testutils').get_config();
/**
* @param ledger_index {Number}
* Expects a corresponding ledger dump in $repo/test/fixtures/ folder
*/
create_ledger_test = function (ledger_index) {
describe(String(ledger_index), function() {
var path = __dirname + '/fixtures/ledger-full-'+ledger_index+'.json';
var ledger_raw = fs.readFileSync(path),
ledger_json = JSON.parse(ledger_raw),
ledger = Ledger.from_json(ledger_json);
it('has account_hash of '+ ledger_json.account_hash, function() {
assert.equal(ledger_json.account_hash,
ledger.calc_account_hash({sanity_test:true}).to_hex());
})
it('has transaction_hash of '+ ledger_json.transaction_hash, function() {
assert.equal(ledger_json.transaction_hash,
ledger.calc_tx_hash().to_hex());
})
})
}
describe('Ledger', function() {
// This is the first recorded ledger with a non empty transaction set
create_ledger_test(38129);
// Because, why not.
create_ledger_test(40000);
describe('#calcAccountRootEntryHash', function () {
it('will calculate the AccountRoot entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
var account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var expectedEntryHash = '2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8';
var actualEntryHash = Ledger.calcAccountRootEntryHash(account);
assert.equal(actualEntryHash.to_hex(), expectedEntryHash);
});
});
describe('#calcRippleStateEntryHash', function () {
it('will calculate the RippleState entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh and rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY in USD', function () {
var account1 = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var account2 = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY';
var currency = 'USD';
var expectedEntryHash = 'C683B5BB928F025F1E860D9D69D6C554C2202DE0D45877ADB3077DA4CB9E125C';
var actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency);
var actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency);
assert.equal(actualEntryHash1.to_hex(), expectedEntryHash);
assert.equal(actualEntryHash2.to_hex(), expectedEntryHash);
});
it('will calculate the RippleState entry hash for r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV and rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj in UAM', function () {
var account1 = 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV';
var account2 = 'rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj';
var currency = 'UAM';
var expectedEntryHash = 'AE9ADDC584358E5847ADFC971834E471436FC3E9DE6EA1773DF49F419DC0F65E';
var actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency);
var actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency);
assert.equal(actualEntryHash1.to_hex(), expectedEntryHash);
assert.equal(actualEntryHash2.to_hex(), expectedEntryHash);
});
});
describe('#calcOfferEntryHash', function () {
it('will calculate the Offer entry hash for r32UufnaCGL82HubijgJGDmdE5hac7ZvLw, sequence 137', function () {
var account = 'r32UufnaCGL82HubijgJGDmdE5hac7ZvLw';
var sequence = 137
var expectedEntryHash = '03F0AED09DEEE74CEF85CD57A0429D6113507CF759C597BABB4ADB752F734CE3';
var actualEntryHash = Ledger.calcOfferEntryHash(account, sequence);
assert.equal(actualEntryHash.to_hex(), expectedEntryHash);
});
});
});
// vim:sw=2:sts=2:ts=8:et

325
test/message-test.js Normal file
View File

@@ -0,0 +1,325 @@
var assert = require('assert');
var sjcl = require('../build/sjcl');
var Message = require('../src/js/ripple/message').Message;
var Seed = require('../src/js/ripple/seed').Seed;
var Remote = require('../src/js/ripple/remote').Remote;
describe('Message', function(){
describe('signMessage', function(){
it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call signHash', function(){
var normal_signHash = Message.signHash;
var message_text = 'Hello World!';
var signHash_called = false;
Message.signHash = function(hash) {
signHash_called = true;
assert.deepEqual(hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + message_text));
};
Message.signMessage(message_text);
assert(signHash_called);
Message.signHash = normal_signHash;
});
});
describe('signHash', function(){
it('should accept the hash as either a hex string or a bitArray', function(){
var normal_random = sjcl.random.randomWords;
sjcl.random.randomWords = function(num_words){
var words = [];
for (var w = 0; w < num_words; w++) {
words.push(sjcl.codec.hex.toBits('00000000'));
}
return words;
};
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
var signature1 = Message.signHash(hash, secret_string);
var signature2 = Message.signHash(sjcl.codec.hex.toBits(hash), secret_string);
assert.strictEqual(signature1, signature2);
sjcl.random.randomWords = normal_random;
});
it('should accept the secret as a string or scjl.ecc.ecdsa.secretKey object', function(){
var normal_random = sjcl.random.randomWords;
sjcl.random.randomWords = function(num_words){
var words = [];
for (var w = 0; w < num_words; w++) {
words.push(sjcl.codec.hex.toBits('00000000'));
}
return words;
};
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
var signature1 = Message.signHash(hash, secret_string);
var signature2 = Message.signHash(hash, Seed.from_json(secret_string).get_key()._secret);
assert.strictEqual(signature1, signature2);
sjcl.random.randomWords = normal_random;
});
it('should throw an error if given an invalid secret key', function(){
var secret_string = 'badsafRpB5euNL52PZPTSqrE9gvuFwTC';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
assert.throws(function(){
Message.signHash(hash, secret_string);
}, /Cannot\ generate\ keys\ from\ invalid\ seed/);
});
it('should throw an error if the parameters are reversed', function(){
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
assert.throws(function(){
Message.signHash(secret_string, hash);
}, Error);
assert.throws(function(){
Message.signHash(secret_string, sjcl.codec.hex.toBits(hash));
}, Error);
assert.throws(function(){
Message.signHash(Seed.from_json(secret_string).get_key()._secret, hash);
}, Error);
assert.throws(function(){
Message.signHash(Seed.from_json(secret_string).get_key()._secret, sjcl.codec.hex.toBits(hash));
}, Error);
});
it('should produce a base64-encoded signature', function(){
var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/;
var normal_random = sjcl.random.randomWords;
sjcl.random.randomWords = function(num_words){
var words = [];
for (var w = 0; w < num_words; w++) {
words.push(sjcl.codec.hex.toBits('00000000'));
}
return words;
};
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
var signature = Message.signHash(hash, secret_string);
assert(REGEX_BASE64.test(signature));
sjcl.random.randomWords = normal_random;
});
});
describe('verifyMessageSignature', function(){
it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call verifyHashSignature', function(){
var normal_verifyHashSignature = Message.verifyHashSignature;
var data = {
message: 'Hello world!',
signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I='
};
var verifyHashSignature_called = false;
Message.verifyHashSignature = function(vhs_data, remote, callback) {
verifyHashSignature_called = true;
assert.deepEqual(vhs_data.hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message));
assert.strictEqual(vhs_data.signature, data.signature);
callback();
};
Message.verifyMessageSignature(data, {}, function(err){
assert(!err);
});
assert(verifyHashSignature_called);
Message.verifyHashSignature = normal_verifyHashSignature;
});
});
describe('verifyHashSignature', function(){
it('should throw an error if a callback function is not supplied', function(){
var data = {
message: 'Hello world!',
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
assert.throws(function(){
Message.verifyHashSignature(data);
}, /(?=.*callback\ function).*/);
});
it('should respond with an error if the hash is missing or invalid', function(done){
var data = {
message: 'Hello world!',
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
assert(/hash/i.test(err.message));
done();
});
});
it('should respond with an error if the account is missing or invalid', function(done){
var data = {
message: 'Hello world!',
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk='
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
assert(/account|address/i.test(err.message));
done();
});
});
it('should respond with an error if the signature is missing or invalid', function(done){
var data = {
message: 'Hello world!',
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
assert(/signature/i.test(err.message));
done();
});
});
it('should respond true if the signature is valid and corresponds to an active public key for the account', function(done){
var data = {
message: 'Hello world!',
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs='
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) {
if (account === data.account) {
callback(null, {
"account_data": {
"Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz",
"Flags": 1114112,
"LedgerEntryType": "AccountRoot",
"RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf"
}
});
} else {
callback(new Error('wrong account'));
}
};
Message.verifyHashSignature(data, test_remote, function(err, valid){
assert(!err);
assert(valid);
done();
});
});
it('should respond false if a key can be recovered from the signature but it does not correspond to an active public key', function(done){
// Signature created by disabled master key
var data = {
message: 'Hello world!',
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk='
};
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) {
if (account === data.account) {
callback(null, {
"account_data": {
"Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz",
"Flags": 1114112,
"LedgerEntryType": "AccountRoot",
"RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf"
}
});
} else {
callback(new Error('wrong account'));
}
};
Message.verifyHashSignature(data, test_remote, function(err, valid){
assert(!err);
assert(!valid);
done();
});
});
});
});

View File

@@ -1,45 +1,186 @@
var assert = require('assert');
var utils = require('./testutils');
var sinon = require('sinon');
var Remote = utils.load_module('remote').Remote;
var Server = utils.load_module('server').Server;
var Request = utils.load_module('request').Request;
var options, spy, mock, stub, remote, callback;
var options, spy, mock, stub, remote, callback, database, tx;
describe('Remote', function () {
describe('initialing a remote with options', function () {
beforeEach(function () {
options = {
trace : true,
trusted: true,
local_signing: true,
servers: [
{ host: 's-west.ripple.com', port: 443, secure: true },
{ host: 's-east.ripple.com', port: 443, secure: true }
],
beforeEach(function () {
options = {
trace : true,
trusted: true,
local_signing: true,
blobvault : 'https://blobvault.payward.com',
persistent_auth : false,
transactions_per_page: 50,
servers: [
{ host: 's-west.ripple.com', port: 443, secure: true },
{ host: 's-east.ripple.com', port: 443, secure: true }
],
bridge: {
out: {
// 'bitcoin': 'localhost:3000'
// 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/'
}
},
blobvault : 'https://blobvault.payward.com',
persistent_auth : false,
transactions_per_page: 50,
};
})
it('should add a server for each specified', function (done) {
var remote = new Remote(options);
done();
})
bridge: {
out: {
// 'bitcoin': 'localhost:3000'
// 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/'
}
},
};
})
describe('functions that return request objects', function () {
describe('remote server initialization - url object', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443, secure: true } ],
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - no secure property', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443 } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - secure: false', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443, secure: false } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - string port', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: '443', secure: true } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - invalid host', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: '+', port: 443, secure: true } ]
});
}, Error);
done();
})
});
describe('remote server initialization - url object - invalid port', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: null, secure: true } ]
});
}, TypeError);
done();
})
});
describe('remote server initialization - url object - port out of range', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 65537, secure: true } ]
});
}, Error);
done();
})
});
describe('remote server initialization - url string', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ 'wss://s-west.ripple.com:443' ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url string - ws://', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:443' ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url string - invalid host', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://+:443' ]
});
}, Error
);
done();
})
});
describe('remote server initialization - url string - invalid port', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:null' ]
});
}, Error
);
done();
})
});
describe('remote server initialization - url string - port out of range', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:65537:' ]
});
}, Error
);
done();
})
});
describe('request constructors', function () {
beforeEach(function () {
callback = function () {}
remote = new Remote(options);
@@ -101,4 +242,91 @@ describe('Remote', function () {
});
});
})
describe('create remote and get pending transactions', function() {
before(function() {
tx = [{
tx_json: {
Account : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Amount : {
currency : "LTC",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "9.985"
},
Destination : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Fee : "15",
Flags : 0,
Paths : [
[
{
account : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
currency : "USD",
issuer : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
type : 49,
type_hex : "0000000000000031"
},
{
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 48,
type_hex : "0000000000000030"
},
{
account : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 49,
type_hex : "0000000000000031"
}
]
],
SendMax : {
currency : "USD",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "30.30993068"
},
Sequence : 415,
SigningPubKey : "02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A",
TransactionType : "Payment",
TxnSignature : "304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"
},
clientID: '48631',
state: 'pending',
submitIndex: 1,
submittedIDs: ["304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"],
secret: 'mysecret'
}];
database = {
getPendingTransactions: function(callback) {
callback(null, tx);
}
}
})
it('should set transaction members correct ', function(done) {
remote = new Remote(options);
remote.storage = database;
remote.transaction = function() {
return {
clientID: function(id) {
if (typeof id === 'string') {
this._clientID = id;
}
return this;
},
submit: function() {
assert.deepEqual(this._clientID, tx[0].clientID);
assert.deepEqual(this.submittedIDs,[tx[0].tx_json.TxnSignature]);
assert.equal(this.submitIndex, tx[0].submitIndex);
assert.equal(this.secret, tx[0].secret);
done();
},
parseJson: function(json) {}
}
}
remote.getPendingTransactions();
})
})
})

676
test/request-test.js Normal file
View File

@@ -0,0 +1,676 @@
var assert = require('assert');
var utils = require('./testutils');
var Request = utils.load_module('request').Request;
var Remote = utils.load_module('remote').Remote;
var Server = utils.load_module('server').Server;
function makeServer(url) {
var server = new Server(new process.EventEmitter(), url);
server._connected = true;
return server;
};
const SERVER_INFO = {
'info': {
'build_version': '0.25.2-rc1',
'complete_ledgers': '32570-7016339',
'hostid': 'LIED',
'io_latency_ms': 1,
'last_close': {
'converge_time_s': 2.013,
'proposers': 5
},
'load_factor': 1,
'peers': 42,
'pubkey_node': 'n9LpxYuMx4Epz4Wz8Kg2kH3eBTx1mUtHnYwtCdLoj3HC85L2pvBm',
'server_state': 'full',
'validated_ledger': {
'age': 0,
'base_fee_xrp': 0.00001,
'hash': 'E43FD49087B18031721D9C3C4743FE1692C326AFF7084A2C01B355CE65A4C699',
'reserve_base_xrp': 20,
'reserve_inc_xrp': 5,
'seq': 7016339
},
'validation_quorum': 3
}
};
describe('Request', function() {
it('Send request', function(done) {
var remote = {
request: function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
done();
}
};
var request = new Request(remote, 'server_info');
request.request();
// Should only request once
request.request();
});
it('Broadcast request', function(done) {
var servers = [
makeServer('wss://localhost:5006'),
makeServer('wss://localhost:5007')
];
var requests = 0;
servers.forEach(function(server, index, arr) {
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
if (++requests === arr.length) {
done();
}
};
});
var remote = new Remote();
remote._connected = true;
remote._servers = servers;
var request = new Request(remote, 'server_info');
request.broadcast();
});
it('Events API', function(done) {
var server = makeServer('wss://localhost:5006');
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
req.emit('success', SERVER_INFO);
};
var remote = new Remote();
remote._connected = true;
remote._servers = [ server ];
var request = new Request(remote, 'server_info');
request.once('success', function(res) {
assert.deepEqual(res, SERVER_INFO);
done();
});
request.request();
});
it('Callback API', function(done) {
var server = makeServer('wss://localhost:5006');
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
req.emit('success', SERVER_INFO);
};
var remote = new Remote();
remote._connected = true;
remote._servers = [ server ];
var request = new Request(remote, 'server_info');
request.callback(function(err, res) {
assert.ifError(err);
assert.deepEqual(res, SERVER_INFO);
done();
});
});
it('Timeout', function(done) {
var server = makeServer('wss://localhost:5006');
var successEmited = false;
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
setTimeout(function() {
successEmitted = true;
req.emit('success', SERVER_INFO);
}, 200);
};
var remote = new Remote();
remote._connected = true;
remote._servers = [ server ];
var request = new Request(remote, 'server_info');
request.timeout(10, function() {
setTimeout(function() {
assert(successEmitted);
done();
}, 200);
});
request.callback(function(err, res) {
assert(false, 'Callback should not be called');
});
});
it('Timeout - satisfied', function(done) {
var server = makeServer('wss://localhost:5006');
var successEmited = false;
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
setTimeout(function() {
successEmitted = true;
req.emit('success', SERVER_INFO);
}, 200);
};
var remote = new Remote();
remote._connected = true;
remote._servers = [ server ];
var request = new Request(remote, 'server_info');
var timedOut = false;
request.once('timeout', function() {
timedOut = true;
});
request.timeout(1000);
request.callback(function(err, res) {
assert(!timedOut);
assert.ifError(err);
assert.deepEqual(res, SERVER_INFO);
done();
});
});
it('Set server', function(done) {
var servers = [
makeServer('wss://localhost:5006'),
makeServer('wss://localhost:5007')
];
servers[1]._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
done();
};
var remote = new Remote();
remote._connected = true;
remote._servers = servers;
remote.getServer = function() {
return servers[0];
};
var request = new Request(remote, 'server_info');
request.setServer(servers[1]);
assert.strictEqual(request.server, servers[1]);
request.request();
});
it('Set server - by URL', function(done) {
var servers = [
makeServer('wss://localhost:5006'),
makeServer('wss://127.0.0.1:5007')
];
servers[1]._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
done();
};
var remote = new Remote();
remote._connected = true;
remote._servers = servers;
remote.getServer = function() {
return servers[0];
};
var request = new Request(remote, 'server_info');
request.setServer('wss://127.0.0.1:5007');
assert.strictEqual(request.server, servers[1]);
request.request();
});
it('Set build path', function() {
var remote = new Remote();
remote._connected = true;
remote.local_signing = false;
var request = new Request(remote, 'server_info');
request.buildPath(true);
assert.strictEqual(request.message.build_path, true);
});
it('Remove build path', function() {
var remote = new Remote();
remote._connected = true;
remote.local_signing = false;
var request = new Request(remote, 'server_info');
request.buildPath(false);
assert(!request.message.hasOwnProperty('build_path'));
});
it('Set build path with local signing', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
assert.throws(function() {
request.buildPath(true);
}, Error);
});
it('Set ledger hash', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerHash('B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
assert.strictEqual(request.message.ledger_hash, 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
});
it('Set ledger index', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerIndex(7016915);
assert.strictEqual(request.message.ledger_index, 7016915);
});
it('Select cached ledger - index', function() {
var remote = new Remote();
remote._connected = true;
remote._ledger_current_index = 1;
remote._ledger_hash = 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE';
var request = new Request(remote, 'server_info');
request.ledgerChoose(true);
assert.strictEqual(request.message.ledger_index, 1);
});
it('Select cached ledger - hash', function() {
var remote = new Remote();
remote._connected = true;
remote._ledger_current_index = 1;
remote._ledger_hash = 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE';
var request = new Request(remote, 'server_info');
request.ledgerChoose();
assert.strictEqual(request.message.ledger_hash, 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
});
it('Select ledger - identifier', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerSelect('validated');
assert.strictEqual(request.message.ledger_index, 'validated');
});
it('Select ledger - index', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerSelect(7016915);
assert.strictEqual(request.message.ledger_index, 7016915);
});
it('Select ledger - hash', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerSelect('B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
assert.strictEqual(request.message.ledger_hash, 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
});
it('Select ledger - hash', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.ledgerSelect('B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
assert.strictEqual(request.message.ledger_hash, 'B4FD84A73DBD8F0DA9E320D137176EBFED969691DC0AAC7882B76B595A0841AE');
});
it('Set account_root', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accountRoot('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59');
assert.strictEqual(request.message.account_root, 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59');
});
it('Set index', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.index(1);
assert.strictEqual(request.message.index, 1);
});
it('Set offer ID', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.offerId('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 1337);
assert.deepEqual(request.message.offer, {
account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
seq: 1337
});
});
it('Set offer index', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.offerIndex(1337);
assert.strictEqual(request.message.offer, 1337);
});
it('Set secret', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.secret('mySecret');
assert.strictEqual(request.message.secret, 'mySecret');
});
it('Set transaction hash', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.txHash('E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7');
assert.strictEqual(request.message.tx_hash, 'E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7');
});
it('Set transaction JSON', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
var txJson = { hash: 'E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7' };
request.txJson(txJson);
assert.deepEqual(request.message.tx_json, txJson);
});
it('Set transaction blob', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.txBlob('asdf');
assert.strictEqual(request.message.tx_blob, 'asdf');
});
it('Set ripple state', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.rippleState('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'USD');
assert.deepEqual(request.message.ripple_state, {
currency: 'USD',
accounts: [ 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59' ]
});
});
it('Set accounts', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accounts([
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
assert.deepEqual(request.message.accounts, [
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
});
it('Set accounts - string', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accounts('rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun');
assert.deepEqual(request.message.accounts, [
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun'
]);
});
it('Set accounts proposed', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accountsProposed([
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
assert.deepEqual(request.message.accounts_proposed, [
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
});
it('Add account', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accounts([
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
]);
request.addAccount('rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
assert.deepEqual(request.message.accounts, [
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
});
it('Add account proposed', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.accountsProposed([
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
]);
request.addAccountProposed('rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B');
assert.deepEqual(request.message.accounts_proposed, [
'rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun',
'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
]);
});
it('Set books', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
var books = [
{
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
}
];
request.books(books);
assert.deepEqual(request.message.books, books);
});
it('Add book', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.addBook({
'taker_gets': {
'currency': 'CNY',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
});
assert.deepEqual(request.message.books, [
{
'taker_gets': {
'currency': 'CNY',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
}
]);
var books = [
{
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
}
];
request.books(books);
assert.deepEqual(request.message.books, [
{
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
},
]);
});
it('Add book - missing side', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.message.books = void(0);
var books = [
{
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
}
}
];
assert.throws(function() {
request.books(books);
});
});
it('Add book - with snapshot', function() {
var remote = new Remote();
remote._connected = true;
var request = new Request(remote, 'server_info');
request.message.books = void(0);
var book = {
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'both': true
};
request.addBook(book, true);
assert.deepEqual(request.message.books, [{
'taker_gets': {
'currency': 'EUR',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'taker_pays': {
'currency': 'USD',
'issuer': 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
'snapshot': true,
'both': true
}]);
});
});

63
test/seed-test.js Normal file
View File

@@ -0,0 +1,63 @@
var assert = require('assert');
var utils = require('./testutils');
var Seed = utils.load_module('seed').Seed;
var config = require('./testutils').get_config();
describe('Seed', function() {
it('can generate many addresses', function () {
var seed = Seed.from_json("masterpassphrase");
// Note: Created with jRippleAPI code
// Link: https://github.com/pmarches/jRippleAPI/blob/master/src/jrippleapi/keys/RippleDeterministicKeyGenerator.java
// To reviewer/merger: Even generating keypairs for thi this small amount of
// addresses makes the test suite run SO SLOW. Consider cutting at the tail.
var first_nth_addresses = [
// 0th implied
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
// 1th implied
"r4bYF7SLUMD7QgSLLpgJx38WJSY12ViRjP",
// 2th implied
"rLpAd4peHUMBPbVJASMYK5GTBUSwXRD9nx",
// ...
"rN9HWHPAftXC6xNxJRqgVr1HexpUi6FBUa",
"rhwPfeEkeQh2vqoKGPBpPyS9nKmKZdpypu",
"rKdNfPfrzfisDCPCqK67YhDLezuVKGKXNm",
"rsn4NeGzMRvMn5ABQxmt9VNEkSknVneBK4",
"rUSJGFH1kQnaKpoAhkArfyw6HZVe8GT5w3",
"r3EYp6isx2Row5pu19C4Ujvp68oEEabhuA",
"rGiYpnAn9DTXiM78CCJcYAuL6QHEDdazHA",
"rVrRVeqqEJHAove5B9TqBAHEXBTzMi5eu",
"rJbarThDXYXxtCgDxRoTCDf2NoYdgSjuVk",
"rfuWNJ1TkQzoZZcc4K8wmtsUiGwURFbed1",
"rpuTqebfbZZdKEUV3bjecoViNL4W4gepWZ",
"r42ywHUco4C2AjgaSmYM7uX13Kbz6GdDKp",
"rnWb45MLd5hQiB6Arr94DdY2z95quL7XNf",
"rMukaQ87bfAeddcT16RtgS3RbFmaQWrSGu",
"rN6dMHU51K9y8eVMhjQMsQVp5gPwt3eYYx",
"rfRFyeJDNqpfZVFmjNj9tNzFVsCNAWKtNc",
"rMoGSX48KrT31HKx9KfMSnYhjvRw3YYzQW",
"rfTiEfbeVsYU7QEe1Zfogiz7h43x6ChKGC"
]
function assert_helper(account_id, expected) {
var keypair = seed.get_key(account_id);
assert.strictEqual(keypair.get_address().to_json(),
expected);
}
for (var nth = 0; nth < first_nth_addresses.length; nth++) {
var expected = first_nth_addresses[nth], looking_for;
//`seed.get_key($ripple_address)` is arguably an ill concieved feature
// as it needs to generate `nth` many keypairs and generate hashed public
// keys (addresses) for equality tests ??
// Would need remote.set_secret(address, private_key_not_seed) ??
assert_helper((looking_for = expected), expected)
// This isn't too bad as it only needs to generate one keypair `seq`
assert_helper((looking_for = nth), expected)
};
});
});
// vim:sw=2:sts=2:ts=8:et

View File

@@ -3,8 +3,8 @@ var assert = require('assert');
var SerializedObject = utils.load_module('serializedobject').SerializedObject;
describe('Serialized object', function() {
describe('Serialized object', function() {
it('From json and back', function() {
describe('#from_json(v).to_json() == v', function(){
it('outputs same as passed to from_json', function() {
var input_json = {
Account: 'r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS',
Amount: '274579388',
@@ -35,27 +35,60 @@ describe('Serialized object', function() {
assert.deepEqual(input_json, output_json);
});
});
describe('Format validation', function() {
// Peercover actually had a problem submitting transactions without a `Fee`
// and rippled was only informing of "transaction is invalid"
it('should throw an Error when there is a missing field', function() {
describe('#from_json', function() {
it('understands TransactionType as a Number', function() {
var input_json = {
// no non required fields
Account: 'r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS',
Amount: '274579388',
Destination: 'r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS',
Fee: '15',
Sequence: 351,
SigningPubKey: '02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A',
TransactionType: 'Payment',
TxnSignature: '30450221009DA3A42DD25E3B22EC45AD8BA8FC7A954264264A816D300B2DF69F814D7D4DD2022072C9627F97EEC6DA13DE841E06E2CD985EF06A0FBB15DDBF0800D0730C8986BF'
SigningPubKey: '02',// VL field ;)
TransactionType: 0 //
};
assert.throws (
function() {
var output_json = SerializedObject.from_json(input_json);
},
/Payment is missing fields: \["Fee"\]/
);
var output_json = SerializedObject.from_json(input_json).to_json();
assert.equal(0, input_json.TransactionType);
assert.equal("Payment", output_json.TransactionType);
});
});
it('understands LedgerEntryType as a Number', function() {
var input_json = {
// no, non required fields
"LedgerEntryType": 100,
"Flags": 0,
"Indexes": [],
"RootIndex": "000360186E008422E06B72D5B275E29EE3BE9D87A370F424E0E7BF613C465909"
}
var output_json = SerializedObject.from_json(input_json).to_json();
assert.equal(100, input_json.LedgerEntryType);
assert.equal("DirectoryNode", output_json.LedgerEntryType);
});
describe('Format validation', function() {
// Peercover actually had a problem submitting transactions without a `Fee`
// and rippled was only informing of "transaction is invalid"
it('should throw an Error when there is a missing field', function() {
var input_json = {
Account: 'r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS',
Amount: '274579388',
Destination: 'r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS',
Sequence: 351,
SigningPubKey: '02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A',
TransactionType: 'Payment',
TxnSignature: '30450221009DA3A42DD25E3B22EC45AD8BA8FC7A954264264A816D300B2DF69F814D7D4DD2022072C9627F97EEC6DA13DE841E06E2CD985EF06A0FBB15DDBF0800D0730C8986BF'
};
assert.throws (
function() {
var output_json = SerializedObject.from_json(input_json);
},
/Payment is missing fields: \["Fee"\]/
);
});
});
})
});
// vim:sw=2:sts=2:ts=8:et

View File

@@ -282,6 +282,15 @@ describe('Serialized types', function() {
types.Int64.serialize(so, 4294967295.5);
assert.strictEqual(so.to_hex(), '00000000FFFFFFFF');
});
it('Does not get confused when the high bit is set', function () {
var so = new SerializedObject();
types.Int64.serialize(so, "8B2386F26F8E232B");
assert.strictEqual(so.to_hex(), '8B2386F26F8E232B');
var so = new SerializedObject("8B2386F26F8E232B");
var num = types.Int64.parse(so);
// We get a positive number
assert.strictEqual(num.toString(16), '8b2386f26f8e232b');
});
it('Serialize "0123456789ABCDEF"', function () {
var so = new SerializedObject();
types.Int64.serialize(so, '0123456789ABCDEF');
@@ -798,21 +807,16 @@ describe('Serialized types', function() {
var so = new SerializedObject(hex);
var as_json = so.to_json();
var expected_json = {
"LedgerEntryType": "DirectoryNode",
"Owner": "rh6kN9s7spSb3vdv6H8ZGYzsddSLeEUGmc",
"Flags": 0,
"LedgerEntryType": "DirectoryNode",
"Owner": "rh6kN9s7spSb3vdv6H8ZGYzsddSLeEUGmc",
"Flags": 0,
"Indexes": [
"081342A0AB45459A54D8E4FA1842339A102680216CF9A152BCE4F4CE467D8246"
],
],
"RootIndex": "000360186E008422E06B72D5B275E29EE3BE9D87A370F424E0E7BF613C465909"
}
assert.deepEqual(as_json, expected_json);
assert.throws(function () {
// This is an encoded reminder/TODO:
// Serializing ledger entries isn't currently supported, but when it
// is, this should no longer throw, and the test will fail
/*assert.strictEqual(*/SerializedObject.from_json(expected_json).to_hex()/*, hex)*/;
})
assert.strictEqual(SerializedObject.from_json(expected_json).to_hex(), hex)
});
it('Serialize empty object {}', function () {
var so = new SerializedObject();

888
test/server-test.js Normal file
View File

@@ -0,0 +1,888 @@
var assert = require('assert');
var ws = require('ws');
var utils = require('./testutils');
var Remote = utils.load_module('remote').Remote;
var Server = utils.load_module('server').Server;
var Request = utils.load_module('request').Request;
var Transaction = utils.load_module('transaction').Transaction;
describe('Server', function() {
it('Server constructor - invalid options', function() {
assert.throws(function() {
var server = new Server(new Remote());
});
});
it('Message listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._handleMessage = function(message) {
assert.strictEqual(typeof message, 'string');
assert.deepEqual(JSON.parse(message).result, {});
done();
};
server.emit('message', JSON.stringify({result: {}}));
});
it('Subscribe response listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._handleResponseSubscribe = function(message) {
assert.strictEqual(typeof message, 'string');
assert.deepEqual(JSON.parse(message).result, {});
done();
};
server.emit('response_subscribe', JSON.stringify({result: {}}));
});
it('Activity listener', function(done) {
// Activity listener should be enabled
var server = new Server(new Remote(), 'wss://localhost:5006');
server.emit('ledger_closed');
var interval = setInterval(function(){}, Infinity);
assert.deepEqual(server._activityInterval.__proto__, interval.__proto__);
clearInterval(interval);
done();
});
it('Reconnect activity listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server.emit('ledger_closed');
var interval = setInterval(function(){}, Infinity);
assert.deepEqual(server._activityInterval.__proto__, interval.__proto__);
server.once('disconnect', function() {
// Interval should clear
assert.deepEqual(server._activityInterval.__proto__, interval.__proto__);
assert.strictEqual(server._activityInterval._onTimeout, null)
server.once('ledger_closed', function() {
// Interval should be reset
assert.deepEqual(server._activityInterval.__proto__, interval.__proto__);
assert.strictEqual(typeof server._activityInterval._onTimeout, 'function');
done();
});
server.emit('ledger_closed');
});
server.emit('disconnect');
});
it('Update server score ledger close listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
var ledger = {
'type': 'ledgerClosed',
'fee_base': 10,
'fee_ref': 10,
'ledger_hash': 'D29E1F2A2617A88E9DAA14F468B169E6875092ECA0B3B1FA2BE1BC5524DE7CB2',
'ledger_index': 7035609,
'ledger_time': 455327690,
'reserve_base': 20000000,
'reserve_inc': 5000000,
'txn_count': 1,
'validated_ledgers': '32570-7035609'
};
server._updateScore = function(type, data) {
assert.strictEqual(type, 'ledgerclose');
assert.deepEqual(data, ledger);
done();
};
server._remote.emit('ledger_closed', ledger);
});
it('Update server score ping listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
var ping = {
time: 500
};
server._updateScore = function(type, data) {
assert.strictEqual(type, 'response');
assert.deepEqual(data, ping);
done();
};
server.emit('response_ping', {}, ping);
});
it('Update server score load listener', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
var load = {
'fee_base': 10,
'fee_ref': 10
};
server._updateScore = function(type, data) {
assert.strictEqual(type, 'loadchange');
assert.deepEqual(data, load);
done();
};
server.emit('load_changed', load);
});
it('Websocket constructor', function() {
assert.strictEqual(Server.websocketConstructor(), require('ws'));
});
it('Set state online', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._state = 'offline';
server.once('connect', function() {
assert(server._connected);
done();
});
server._setState('online');
});
it('Set state offline', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._state = 'online';
server.once('disconnect', function() {
assert(!server._connected);
done();
});
server._setState('offline');
});
it('Set state same state', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._state = 'online';
server.once('state', function(state) {
assert(!server._connected);
assert.strictEqual(state, 'offline');
done();
});
server._setState('online');
server._setState('offline');
});
it('Check activity - inactive', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
server.reconnect = function() {
done();
};
server._lastLedgerClose = Date.now() - 1000 * 30;
server._checkActivity();
});
it('Check activity - unconnected', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = false;
server.reconnect = function() {
assert(false, 'Should not reconnect');
};
server._lastLedgerClose = Date.now() - 1000 * 30;
server._checkActivity();
setImmediate(function() {
done();
});
});
it('Check activity - uninitialized', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = false;
server.reconnect = function() {
assert(false, 'Should not reconnect');
};
//server._lastLedgerClose = Date.now() - 1000 * 30;
server._checkActivity();
setImmediate(function() {
done();
});
});
it('Check activity - sufficient ledger close', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = false;
server.reconnect = function() {
assert(false, 'Should not reconnect');
};
server._lastLedgerClose = Date.now() - 1000 * 20;
server._checkActivity();
setImmediate(function() {
done();
});
});
it('Update score - response', function() {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
assert.deepEqual(server._scoreWeights, {
ledgerclose: 5,
response: 1
});
assert.strictEqual(server._score, 0);
server._updateScore('response', { time: Date.now() - 1000 });
// 1000ms second ping / 200ms * weight of 1
assert.strictEqual(server._score, 5);
});
it('Update score - ledger', function() {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
server._lastLedgerIndex = 1;
assert.deepEqual(server._scoreWeights, {
ledgerclose: 5,
response: 1
});
assert.strictEqual(server._score, 0);
server._updateScore('ledgerclose', { ledger_index: 5 });
// Four ledgers behind the leading ledger * weight of 5
assert.strictEqual(server._score, 20);
});
it('Update score - load', function() {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
server._fee_cushion = 1;
assert.deepEqual(server._scoreWeights, {
ledgerclose: 5,
response: 1
});
assert.strictEqual(server._fee, 10);
server.emit('message', {
type: 'serverStatus',
load_base: 256 * 1,
load_factor: 256 * 10,
server_status: 'full'
});
//server._updateScore('loadchange', { });
assert.strictEqual(server._fee, 100);
});
it('Update score - reaching reconnect threshold', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._lastLedgerIndex = 1;
server._connected = true;
server.reconnect = function() {
done();
};
assert.deepEqual(server._scoreWeights, {
ledgerclose: 5,
response: 1
});
assert.strictEqual(server._score, 0);
server._updateScore('ledgerclose', { ledger_index: 250 });
// Four ledgers behind the leading ledger * weight of 5
assert.strictEqual(server._score, 1245);
});
it('Get remote address', function() {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
server._ws = {
_socket: {
remoteAddress: '127.0.0.1'
}
};
assert.strictEqual(server._remoteAddress(), '127.0.0.1');
});
it('Disconnect', function(done) {
var server = new Server(new Remote(), 'wss://localhost:5006');
server._connected = true;
server._ws = {
close: function() {
assert(!server._shouldConnect);
assert.strictEqual(server._state, 'offline');
done();
}
};
server.disconnect();
});
it('Connect', function(done) {
var wss = new ws.Server({ port: 5748 });
wss.once('connection', function(ws) {
ws.once('message', function(message) {
var m = JSON.parse(message);
assert.deepEqual(m, {
command: 'subscribe',
id: 0,
streams: [ 'ledger', 'server' ]
});
ws.send(JSON.stringify({
'id': 0,
'status': 'success',
'type': 'response',
'result': {
'fee_base': 10,
'fee_ref': 10,
'ledger_hash': '1838539EE12463C36F2C53B079D807C697E3D93A1936B717E565A4A912E11776',
'ledger_index': 7053695,
'ledger_time': 455414390,
'load_base': 256,
'load_factor': 256,
'random': 'E56C9154D9BE94D49C581179356C2E084E16D18D74E8B09093F2D61207625E6A',
'reserve_base': 20000000,
'reserve_inc': 5000000,
'server_status': 'full',
'validated_ledgers': '32570-7053695'
}
}));
wss.close();
});
});
var server = new Server(new Remote(), 'ws://localhost:5748');
server.once('connect', function() {
server.once('disconnect', function() {
done();
});
server.disconnect();
});
server.connect();
});
it('Connect - already connected', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = true;
server.once('connect', function() {
assert(false, 'Should not connect');
});
server.connect();
setImmediate(function() {
done();
});
});
it('Connect - prior WebSocket connection exists', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = false;
server._ws = {
close: function() {
done();
}
};
server.connect();
});
it('Connect - no WebSocket constructor', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = false;
var websocketConstructor = Server.websocketConstructor;
Server.websocketConstructor = function() {
return void(0);
};
assert.throws(function() {
server.connect();
}, Error);
Server.websocketConstructor = websocketConstructor;
});
it('Reconnect', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = true;
server._ws = { };
var disconnected = false;
server.disconnect = function() {
disconnected = true;
server.emit('disconnect');
};
server.connect = function() {
assert(disconnected);
done();
};
server.reconnect();
});
it('Retry connect', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = false;
server._shouldConnect = true;
server.connect = function() {
done();
};
server._retryConnect();
var timeout = setTimeout(function(){}, Infinity);
assert.deepEqual(server._retryTimer.__proto__, timeout.__proto__);
clearTimeout(timeout);
});
it('Handle close', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._ws = { };
server._handleClose();
var noOp = (function noOp(){}).toString();
var coverageRE = /__cov_.+;/;
assert.strictEqual(server._ws.onopen.toString().replace(coverageRE, ''), noOp);
assert.strictEqual(server._ws.onclose.toString().replace(coverageRE, ''), noOp);
assert.strictEqual(server._ws.onmessage.toString().replace(coverageRE, ''), noOp);
assert.strictEqual(server._ws.onerror.toString().replace(coverageRE, ''), noOp);
assert.strictEqual(server._state, 'offline');
});
it('Handle error', function(done) {
var wss = new ws.Server({ port: 5748 });
wss.once('connection', function(ws) {
ws.once('message', function(message) {
var m = JSON.parse(message);
assert.deepEqual(m, {
command: 'subscribe',
id: 0,
streams: [ 'ledger', 'server' ]
});
ws.send(JSON.stringify({
'id': 0,
'status': 'success',
'type': 'response',
'result': {
'fee_base': 10,
'fee_ref': 10,
'ledger_hash': '1838539EE12463C36F2C53B079D807C697E3D93A1936B717E565A4A912E11776',
'ledger_index': 7053695,
'ledger_time': 455414390,
'load_base': 256,
'load_factor': 256,
'random': 'E56C9154D9BE94D49C581179356C2E084E16D18D74E8B09093F2D61207625E6A',
'reserve_base': 20000000,
'reserve_inc': 5000000,
'server_status': 'full',
'validated_ledgers': '32570-7053695'
}
}));
});
});
var server = new Server(new Remote(), 'ws://localhost:5748');
server.once('connect', function() {
server._retryConnect = function(){
wss.close();
done();
};
server._ws.emit('error', new Error());
});
server.connect();
});
it('Handle message - ledgerClosed', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
var ledger = {
type: 'ledgerClosed',
ledger_index: 1
};
server.once('ledger_closed', function(message) {
assert.strictEqual(server._lastLedgerIndex, ledger.ledger_index);
done();
});
server.emit('message', ledger);
});
it('Handle message - serverStatus', function(done) {
var remote = new Remote();
var server = new Server(remote, 'ws://localhost:5748');
var events = 3;
var receivedEvents = 0;
var status = {
type: 'serverStatus',
load_base: 256,
load_factor: 256 * 2
};
server.once('load', function(message) {
assert.deepEqual(message, status);
if (++receivedEvents == events) {
done();
}
});
server.once('load_changed', function(message) {
assert.deepEqual(message, status);
if (++receivedEvents == events) {
done();
}
});
remote.once('load_changed', function(message) {
assert.deepEqual(message, status);
if (++receivedEvents == events) {
done();
}
});
server.emit('message', status);
});
it('Handle message - serverStatus - no load', function(done) {
var remote = new Remote();
var server = new Server(remote, 'ws://localhost:5748');
var events = 3;
var receivedEvents = 0;
var status = {
type: 'serverStatus'
};
server.once('load', function(message) {
assert.deepEqual(message, status);
});
server.once('load_changed', function(message) {
assert(false, 'Non-load status should not trigger events');
});
remote.once('load_changed', function(message) {
assert(false, 'Non-load status should not trigger events');
});
server.emit('message', status);
setImmediate(function() {
done();
});
});
it('Handle message - response - success', function(done) {
var remote = new Remote();
var server = new Server(remote, 'ws://localhost:5748');
var request = new Request(remote, 'server_info');
var id = 1;
assert(request instanceof process.EventEmitter);
server._requests[id] = request;
var response = {
id: id,
type: 'response',
status: 'success',
result: {
test: 'property'
}
};
var receivedEvents = 0;
var emitters = 3;
request.once('success', function(message) {
assert.deepEqual(message, response.result);
if (++receivedEvents === emitters) {
done();
}
});
server.once('response_server_info', function(message) {
assert.deepEqual(message, response.result);
if (++receivedEvents === emitters) {
done();
}
});
remote.once('response_server_info', function(message) {
assert.deepEqual(message, response.result);
if (++receivedEvents === emitters) {
done();
}
});
server.emit('message', response);
});
it('Handle message - response - error', function(done) {
var remote = new Remote();
var server = new Server(remote, 'ws://localhost:5748');
var request = new Request(remote, 'server_info');
var id = 1;
assert(request instanceof process.EventEmitter);
server._requests[id] = request;
var response = {
id: id,
type: 'response',
status: 'error',
error: {
test: 'property'
}
};
var receivedEvents = 0;
request.once('error', function(message) {
assert.deepEqual(message, {
error: 'remoteError',
error_message: 'Remote reported an error.',
remote: response
});
done();
});
server.emit('message', response);
});
it('Handle message - response - no request', function(done) {
var remote = new Remote();
var server = new Server(remote, 'ws://localhost:5748');
var response = {
id: 1,
type: 'response',
status: 'success',
result: { }
};
Object.defineProperty(response, 'status', {
get: function() {
assert(false, 'Response status should not be checked');
}
});
server.emit('message', response);
setImmediate(function() {
done();
});
});
it('Handle message - path_find', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._handlePathFind = function() {
done();
};
server.emit('message', { type: 'path_find' });
});
it('Handle message - invalid message', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server.once('unexpected', function() {
done();
});
server.emit('message', { butt: 'path_find' });
});
it('Send message', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
var request = {
id: 1,
message: {
command: 'server_info'
}
};
server._ws = {
send: function(message) {
assert.deepEqual(JSON.parse(message), request);
done();
}
};
server._sendMessage(request);
});
it('Request', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = true;
server._ws = { };
var request = {
message: {
command: 'server_info'
}
};
server._sendMessage = function(message) {
done();
};
server._request(request);
});
it('Request - delayed connect', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = false;
server._ws = { };
var request = {
message: {
command: 'server_info'
}
};
server._request(request);
setImmediate(function() {
server._sendMessage = function(message) {
done();
};
server.emit('connect');
});
});
it('Request - no WebSocket', function(done) {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = true;
server._ws = void(0);
var request = {
message: {
command: 'server_info'
}
};
server._sendMessage = function(message) {
assert(false, 'Should not send message if WebSocket does not exist');
};
server._request(request);
setImmediate(function() {
done();
});
});
it('Check connectivity', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._connected = false;
server._ws = { readyState: 1 };
assert(!server._isConnected());
assert(!server._isConnected({ message: { command: 'ping' } }));
assert(server._isConnected({ message: { command: 'subscribe' } }));
server._connected = true;
assert(server._isConnected());
});
it('Compute fee - transaction', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
var transaction = new Transaction();
assert.strictEqual(server._computeFee(transaction), '12');
});
it('Compute fee - fee units', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
var transaction = new Transaction();
assert.strictEqual(server._computeFee(10), '12');
});
it('Compute fee - bad arg', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
var transaction = new Transaction();
assert.throws(function() {
server._computeFee('asdf');
});
});
it('Compute fee - increased load', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._load_base = 256;
server._load_factor = 256 * 4;
var transaction = new Transaction();
assert.strictEqual(server._computeFee(transaction), '48');
});
it('Compute reserve', function() {
var server = new Server(new Remote(), 'ws://localhost:5748');
server._reserve_base = 20000000;
server._reserve_inc = 5000000;
assert.strictEqual(server._reserve().to_json(), '20000000');
});
});

View File

@@ -0,0 +1,28 @@
var assert = require('assert');
var utils = require('./testutils');
var sjcl = require('../build/sjcl');
var Seed = require('../src/js/ripple/seed').Seed;
describe('SJCL ECDSA Canonicalization', function() {
describe('canonicalizeSignature', function() {
it('should canonicalize non-canonical signatures', function () {
var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk');
var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ');
var rs = sjcl.codec.hex.toBits("27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0fb0a0747ecc7b4ca46e45b3b32b6b2a066aa0249c027ef11e5bce93dab756549c");
rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs);
assert.strictEqual(sjcl.codec.hex.fromBits(rs), "27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0f4f5f8b813384b35b91ba4c4cd494d5f8500eb84aacc9af1d6403cab218dfeca5");
});
it('should not touch canonical signatures', function () {
var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk');
var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ');
var rs = sjcl.codec.hex.toBits("5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c");
rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs);
assert.strictEqual(sjcl.codec.hex.fromBits(rs), "5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c");
});
});
});
// vim:sw=2:sts=2:ts=8:et

View File

@@ -0,0 +1,245 @@
var assert = require('assert');
var utils = require('./testutils');
var sjcl = require('../build/sjcl');
describe('ECDSA signing with recoverable public key', function(){
describe('Sign and recover public key from signature', function(){
it('should recover public keys from signatures it generates', function(){
var messages = [{
message: 'Hello world!',
secret_hex: '9931c08f61f127d5735fa3c60e702212ce7ed9a2ac90d5dbade99c689728cd9b',
random_value: '5473a3dbdc13ec9efbad7f7f929fbbea404af556a48041dd9d41d29fdbc989ad',
hash_function: sjcl.hash.sha512.hash
// signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I='
}, {
// Correct recovery value for this one is 0
message: 'ua5pdcG0I1JuhSr9Fwai2UoZ9ll5leUtHE5NzSSNnPkw8nSPH5mT1gE1fe0sn',
secret_hex: '84814318ffe6e612694ad59b9084b7b66d68b6979567c619171a67b05e2b654b',
random_value: '14261d30b319709c10ab13cabe595313b99dd2d5c76b8b38d7eb445f0b81cc9a',
hash_function: sjcl.hash.sha512.hash
// signature: 'AAAAHGjpBM7wnTHbPGo0TXsxKbr+d7KvACuJ/eGQsp3ZJfOOQHszaciRo3ClenwKixcquFcBlaVfHlOc3JWOZq1RjpQ='
}, {
// Correct recovery value for this one is 1
message: 'rxc76UnmVTp',
secret_hex: '37eac47c212be8ea8372f506b11673c281cd9ea29a035c2c9e90d027c3dbecc6',
random_value: '61b53ca6de0543f911765ae216a3a4d851918a0733fba9ac80cf29de5bec8032',
hash_function: sjcl.hash.sha256.hash
// signature: 'AAAAG8L/yOA3nNqK4aOiQWJmOaWvkvr3NoTk6wCdX97U3qowdgFd98UK3evWV16qO3RHgFMEnUW/Vt4+kcidqW6hMo0='
}];
var curve = sjcl.ecc.curves['c256'];
for (var m = 0; m < messages.length; m++) {
var message = messages[m].message;
var secret_hex = messages[m].secret_hex;
var random_value = messages[m].random_value;
var hash_function = messages[m].hash_function;
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var pub_val_point = secret_key._curve.G.mult(secret_key._exponent);
var public_key = new sjcl.ecc.ecdsa.publicKey(curve, pub_val_point);
var hash = hash_function(message);
var recoverable_signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, recoverable_signature);
assert.deepEqual(public_key.get().x, recovered_public_key.get().x, 'The x value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().x.toString() + '. Actual: ' + recovered_public_key.get().x.toString());
assert.deepEqual(public_key.get().y, recovered_public_key.get().y, 'The y value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().y.toString() + '. Actual: ' + recovered_public_key.get().y.toString());
}
});
});
describe('signWithRecoverablePublicKey', function(){
// it('should produce the same values as bitcoinjs-lib\'s implementation', function(){
// // TODO: figure out why bitcoinjs-lib and this produce different signature values
// var curve = sjcl.ecc.curves['c256'];
// var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
// var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
// var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
// // var public_key = '0217b9f5b3ba8d550f19fdfb5233818cd27d19aaea029b667f547f5918c307ed3b';
// var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
// var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
// var bitcoin_signature_base64 = 'IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=';
// var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
// var signature_base64 = sjcl.codec.base64.fromBits(signature);
// assert.equal(signature_base64, bitcoin_signature_base64);
// });
it('should produce an error if the hash is not given as a bitArray', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
assert.throws(function(){
secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
}, /(?=.*hash)(?=.*bitArray).+/);
});
it('should return a bitArray', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
assert(typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number');
});
it('should return a bitArray where the first word contains the recovery factor', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var recovery_factor = signature[0] - 27;
assert(recovery_factor >= 0 && recovery_factor < 4);
});
});
describe('recoverFromSignature', function(){
// it('should be able to recover public keys from bitcoinjs-lib\'s implementation', function(){
// // TODO: figure out why bitcoinjs-lib and this produce different signature values
// var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
// var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=');
// var public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
// });
it('should produce an error if the signature given does not have the recovery factor prefix', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.sign(hash, 0, random_value);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
}, /(?=.*signature)(?=.*recovery factor)(?=.*public key).*/);
});
it('should produce an error if it is not given both the hash and the signature', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash);
}, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(signature);
}, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/);
});
it('should produce an error if it cannot generate a valid public key from the the signature', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=');
signature[0] = 27;
signature[3] = 0 - signature[3];
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
}, /(?=.*Cannot\ recover\ public\ key).*/);
});
it('should return a publicKey object', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
assert(key instanceof sjcl.ecc.ecdsa.publicKey);
});
it('tampering with the signature should produce a different public key, if it produces a valid one at all', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
signature[3]++;
var original_public_key = new sjcl.ecc.ecdsa.publicKey(curve, curve.G.mult(secret_key._exponent));
var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
assert.notDeepEqual(original_public_key.get().x, recovered_public_key.get().x);
assert.notDeepEqual(original_public_key.get().y, recovered_public_key.get().y);
});
});
});

View File

@@ -0,0 +1,92 @@
var assert = require('assert');
var utils = require('./testutils');
var Transaction = utils.load_module('transaction').Transaction;
var TransactionQueue = utils.load_module('transactionqueue').TransactionQueue;
describe('Transaction queue', function() {
it('Push transaction', function() {
var queue = new TransactionQueue();
var tx = new Transaction();
queue.push(tx);
assert.strictEqual(queue.length(), 1);
});
it('Remove transaction', function() {
var queue = new TransactionQueue();
var tx = new Transaction();
queue.push(tx);
queue.remove(tx);
assert.strictEqual(queue.length(), 0);
});
it('Remove transaction by ID', function() {
var queue = new TransactionQueue();
var tx = new Transaction();
queue.push(tx);
tx.submittedIDs = [
'1A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B',
'2A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'
];
queue.remove('3A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B');
assert.strictEqual(queue.length(), 1);
queue.remove('2A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B');
assert.strictEqual(queue.length(), 0);
});
it('Add sequence', function() {
var queue = new TransactionQueue();
queue.addReceivedSequence(1);
assert(queue.hasSequence(1));
});
it('Add ID', function() {
var queue = new TransactionQueue();
var tx = new Transaction();
queue.addReceivedId('1A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B', tx);
assert.strictEqual(queue.getReceived('2A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'), void(0));
assert.strictEqual(queue.getReceived('1A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'), tx);
});
it('Get submission', function() {
var queue = new TransactionQueue();
var tx = new Transaction();
queue.push(tx);
tx.submittedIDs = [
'1A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B',
'2A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'
];
assert.strictEqual(queue.getSubmission('1A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'), tx);
assert.strictEqual(queue.getSubmission('2A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'), tx);
assert.strictEqual(queue.getSubmission('3A4DEBF37496464145AA301F0AA77712E3A2BFE3480D24C3584663F800B85B5B'), void(0));
});
it('Iterate over queue', function() {
var queue = new TransactionQueue();
var count = 10;
for (var i=0; i<count; i++) {
queue.push(new Transaction());
}
queue.forEach(function(tx) {
assert(tx instanceof Transaction);
--count;
});
assert.strictEqual(count, 0);
});
});

File diff suppressed because it is too large Load Diff

25
test/uint-test.js Normal file
View File

@@ -0,0 +1,25 @@
var assert = require('assert');
var utils = require('./testutils');
var UInt128 = utils.load_module('uint128').UInt128;
var config = require('./testutils').get_config();
describe('UInt', function() {
describe('128', function() {
describe('#parse_number', function () {
it('should create 00000000000000000000000000000000 when called with 0', function () {
var val = UInt128.from_number(0);
assert.strictEqual(val.to_hex(), '00000000000000000000000000000000');
});
it('should create 00000000000000000000000000000001 when called with 1', function () {
var val = UInt128.from_number(0);
assert.strictEqual(val.to_hex(), '00000000000000000000000000000000');
});
it('should create 000000000000000000000000FFFFFFFF when called with 0xFFFFFFFF', function () {
var val = UInt128.from_number(0xFFFFFFFF);
assert.strictEqual(val.to_hex(), '000000000000000000000000FFFFFFFF');
});
});
});
});
// vim:sw=2:sts=2:ts=8:et

698
test/vault-test.js Normal file
View File

@@ -0,0 +1,698 @@
var assert = require('assert');
var RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt;
var AuthInfo = require('../src/js/ripple/authinfo').AuthInfo;
var VaultClient = require('../src/js/ripple/vaultclient').VaultClient;
var Blob = require('../src/js/ripple/blob').Blob;
var UInt256 = require('../src/js/ripple/uint256').UInt256;
var sjcl = require('../build/sjcl');
var nock = require('nock');
var online = process.argv.indexOf('--online-blobvault') !== -1 ? true : false;
var exampleData = {
id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f',
crypt: 'f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82',
unlock: '3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4',
username: 'exampleUser',
new_username : 'exampleUser-rename',
password: 'pass word',
domain: 'staging.ripple.com',
masterkey : 'ssize4HrSYZShMWBtK6BhALGEk8VH',
email_token : '77825040-9096-4695-9cbc-76720f6a8649',
activateLink : 'https://staging.ripple.com/client/#/register/activate/',
blob: {
url: 'https://id.staging.ripple.com',
id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f',
key: 'f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82',
data: {
auth_secret: 'd0aa918e693080a6a8d0ddc7f4dcf4bc0eecc3c3e3235f16a98661ee9c2e7a58',
account_id: 'raVUps4RghLYkVBcpMaRbVKRTTzhesPXd',
email: 'example@example.com',
contacts: [ ],
created: '2014-05-20T23:39:52.538Z',
apps: [ ],
lastSeenTxDate: 1401925490000,
identityVault: { },
revision: 2199,
encrypted_secret: 'APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm'
}
}
};
var rippleTxtRes = "[authinfo_url]\r\nhttps://id.staging.ripple.com/v1/authinfo";
var authInfoRes = {
body : {
version: 3,
blobvault: 'https://id.staging.ripple.com',
pakdf: {
modulus: 'c7f1bc1dfb1be82d244aef01228c1409c1988943ca9e21431f1669b4aa3864c9f37f3d51b2b4ba1ab9e80f59d267fda1521e88b05117993175e004543c6e3611242f24432ce8efa3b81f0ff660b4f91c5d52f2511a6f38181a7bf9abeef72db056508bbb4eeb5f65f161dd2d5b439655d2ae7081fcc62fdcb281520911d96700c85cdaf12e7d1f15b55ade867240722425198d4ce39019550c4c8a921fc231d3e94297688c2d77cd68ee8fdeda38b7f9a274701fef23b4eaa6c1a9c15b2d77f37634930386fc20ec291be95aed9956801e1c76601b09c413ad915ff03bfdc0b6b233686ae59e8caf11750b509ab4e57ee09202239baee3d6e392d1640185e1cd',
alpha: '7283d19e784f48a96062271a5fa6e2c3addf14e6ezf78a4bb61364856d580f13552008d7b9e3b60ebd9555e9f6c7778ec69f976757d206134e54d61ba9d588a7e37a77cf48060522478352d76db000366ef669a1b1ca93c5e3e05bc344afa1e8ccb15d3343da94180dccf590c2c32408c3f3f176c8885e95d988f1565ee9b80c12f72503ab49917792f907bbb9037487b0afed967fefc9ab090164597fcd391c43fab33029b38e66ff4af96cbf6d90a01b891f856ddd3d94e9c9b307fe01e1353a8c30edd5a94a0ebba5fe7161569000ad3b0d3568872d52b6fbdfce987a687e4b346ea702e8986b03b6b1b85536c813e46052a31ed64ec490d3ba38029544aa',
url: 'https://auth1.ripple.com/api/sign',
exponent: '010001',
host: 'auth1.ripple.com'
},
exists: true,
username: 'exampleUser',
address: 'raVUps4RghLYkVBcpMaRbVKRTTzhesPXd',
emailVerified: true,
reserved: false
}
};
var authInfoNewUsernameRes = {
body : {
version: 3,
blobvault: 'https://id.staging.ripple.com',
pakdf: {
modulus: 'c7f1bc1dfb1be82d244aef01228c1409c1988943ca9e21431f1669b4aa3864c9f37f3d51b2b4ba1ab9e80f59d267fda1521e88b05117993175e004543c6e3611242f24432ce8efa3b81f0ff660b4f91c5d52f2511a6f38181a7bf9abeef72db056508bbb4eeb5f65f161dd2d5b439655d2ae7081fcc62fdcb281520911d96700c85cdaf12e7d1f15b55ade867240722425198d4ce39019550c4c8a921fc231d3e94297688c2d77cd68ee8fdeda38b7f9a274701fef23b4eaa6c1a9c15b2d77f37634930386fc20ec291be95aed9956801e1c76601b09c413ad915ff03bfdc0b6b233686ae59e8caf11750b509ab4e57ee09202239baee3d6e392d1640185e1cd',
alpha: '7283d19e784f48a96062271a5fa6e2c3addf14e6ezf78a4bb61364856d580f13552008d7b9e3b60ebd9555e9f6c7778ec69f976757d206134e54d61ba9d588a7e37a77cf48060522478352d76db000366ef669a1b1ca93c5e3e05bc344afa1e8ccb15d3343da94180dccf590c2c32408c3f3f176c8885e95d988f1565ee9b80c12f72503ab49917792f907bbb9037487b0afed967fefc9ab090164597fcd391c43fab33029b38e66ff4af96cbf6d90a01b891f856ddd3d94e9c9b307fe01e1353a8c30edd5a94a0ebba5fe7161569000ad3b0d3568872d52b6fbdfce987a687e4b346ea702e8986b03b6b1b85536c813e46052a31ed64ec490d3ba38029544aa',
url: 'https://auth1.ripple.com/api/sign',
exponent: '010001',
host: 'auth1.ripple.com'
},
exists: false,
username: exampleData.new_username,
emailVerified: false,
reserved: false
}
};
var signRes = '{"result":"success","signres":"64e9e46618fff0b720b8162e6caa209e046af128b929b766d3be421d3f048ba523453dad42597dcec01f23a5080c16695f6209d39a03668d46b782409e4a53821f70b5e6f7c8fd28eb641c504f9f9b2f378bf2ea7f19950790ac6a8832e2659800f5bb06b735bd450fa47b499fbcebeb3b0fc327619dd2171fa40fb0a41d9bcd69dd29567fa94e9466d4674b908f1cfc43822b38b94534cb37eead183b11b33761a73d78be6ba6f3a53291d4154ca0891fa59da58380e05a1e85b15a24d12406795385bcc5a6360a24ecbf068ff6f02097cd917281972d4895769f3a8668b852ea5d4232050200bcd03934f49ea0693d832980614dff1ead67ca2e0ce9073c25","modulus":"c7f1bc1dfb1be82d244aef01228c1409c198894eca9e21430f1669b4aa3864c9f37f3d51b2b4ba1ab9e80f59d267fda1521e88b05117993175e004543c6e3611242f24432ce8efa3b81f0ff660b4f91c5d52f2511a6f38181a7bf9abeef72db056508bbb4eeb5f65f161dd2d5b439655d2ae7081fcc62fdcb281520911d96700c85cdaf12e7d1f15b55ade867240722425198d4ce39019550c4c8a921fc231d3e94297688c2d77cd68ee8fdeda38b7f9a274701fef23b4eaa6c1a9c15b2d77f37634930386fc20ec291be95aed9956801e1c76601b09c413ad915ff03bfdc0b6b233686ae59e8caf11750b509ab4e57ee09202239baee3d6e392d1640185e1cd","alpha":"7283d19e784f48a96062271a4fa6e2c3addf14e6edf78a4bb61364856d580f13552008d7b9e3b60ebd9555e9f6c7778ec69f976757d206134e54d61ba9d588a7e37a77cf48060522478352d76db000366ef669a1b1ca93c5e3e05bc344afa1e8ccb15d3343da94180dccf590c2c32408c3f3f176c8885e95d988f1565ee9b80c12f72503ab49917792f907bbb9037487b0afed967fefc9ab090164597fcd391c43fab33029b38e66ff4af96cbf6d90a01b891f856ddd3d94e9c9b307fe01e1353a8c30edd5a94a0ebba5fe7161569000ad3b0d3568872d52b6fbdfce987a687e4b346ea702e8986b03b6b1b85536c813e46052a31ed64ec490d3ba38029544aa","exponent":"010001"}';
var blobRes = {
body : {
result: 'success',
encrypted_secret: 'APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm',
blob: 'ALXga/k8mgvPpZCY0zJZdaqlptHUBL0E4V/90p4edvb7eCucU2M7aFsHIl3Z3UDu9MdlDnDU42/C+YKL1spkSTPb3rGWr0kXIFmRu8xDAd+OA3Ot7u3OBq0sN2BUHbEc47WiCue84XQHTgBh9tdeiRTqm90LJ7hZ1pD0oqr823YpFguwcC1inxFbSTNxIdWSoC3XCqZtRFM2Y5ALleWhaWKc3OwaFU6yPRcW05IBvTY/7a2SfZyklvXnJh7Bg+vfvz7ms8UCybmBgHlBPY/UqGOdZI6iFGrEQrDMFHgbxwf7bTTiaOM7Su3OsqhM1k90LvQgk3b1olb1VIMZ5J1UuTtOVTpLSsIlzgMvxxdUUyN2zMkeDE3t8kHOThhwWbLG6O+s9F9fktIv4NtoAm0dG9LtkSE1YXajk0qIYr/zrblJy7pEvNv+EzdSr+dpvssmPshgwxoHwvCwae0vL7UTmrCxIWLlHbsbU2uAzgvudJL0WOpX4W+R43U3sgMD2XysKgX783Sa7DLUWCk3Rk9eGp7c3k/XpI0IWvuKzMxID8VzdyMmXP0RE77uUufisBDtwr8gPGzS5kzU3Z/FG/dHJkfBLZdbHOffQTPKO9DKUjztWpx7CTAkN9O21XrdLK1FRAtWFTuvlA66sDYtHRaqglzFjt9DsJk7PAKi1odHeLBmYob/Bs5eK9yNnlLwu3JpHLH/jKxkuxcZ3NEdTm1WPjTdNlvT7kdGAIG9c0vIywEABkQh1kPDOe39h3GRGcUqVWJcMjJmdVDrQH7BBVV+VptCVtMOo1LviaD0MIWMiYdZyGeH5x+FkpAMjKDB3cCUkmxmis8lrDiMlnTZ5Czj+bDPp62Looc7cr2pTR2niFZRosYNgUPx6cAh7tn64RDaa/spAyv0mWyD1qRA8H0sEPmC7m7EPaBIQpODh1NFg/Bxunh+QGSmy9deINB78b9A9zLS6qWljrzg5fMDUN66xRUUKJMSD9+QJePsM4pb60vbnBBtbe04JzY7iOc/CxiT0Px6/1jlSmnY6SCtaFqtDgmQ5MLGTm1tA+aj6caT6FWsXrBboXt3eXRDPHTN+ciKELx7M3dpd4mKVWhBu7nnnVMEu1rSUrmtUStXQHod/C7vVRF2EU1hhTW7ou0hvLn+7xs9B76QeVG7iYFLiZH1qgs+upqnLCnmY3ug9yd9GQ6YwbVL1hbXJLadaOg7qhKst0KXjjjcE4G9AIEyI+UCxGdc/0PNPOCCeYEPshvonCgElGo/fAbaIuSSOFfusiovYffJ0rCkq1RagH0R/llWtUEFEDR5YFVlD3DqK6B22fQK',
revision: 2191,
email: 'example@example.com',
quota: -2975,
patches: []
}
};
var recoverRes = {
body: {
encrypted_secret: 'AAd69B9En2OF4O4LsjD+pFNeJHEGIuLh2hbla58zGvN7qU/16bDfy0QlFj8/Gu++AdFwH5U6',
revision: 2403,
blob_id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f',
blob: 'AFfW9vuHJ2J5UMnEl4WrVIT9z2d+PPVNNHkqzN64b3pKDQcRPFp8vVEqL9B+YVs/KHhFVFNxxCNyVXwO/yGg4BAslYl8Ioo11IODmOltJmb94oKR/JVyfaY4bDWaOzAoa5N/c9LHpmd0L+9igK1o260MK5OZW4BQ6EG7I+8cYi5uM2CLguiddySu2yTEnyHW47zspWP33y2deh6p5mHtLdii/tmlm7b2rKpzrRVuLN/J09jqilhMxlCEr4X065YZLlQapJ45UWvpifejEw/6Qgl1WngZxwifHa504aR/QYhb1XCNeYbkjQ1MmkTmTef47Al4r/Irzoe//pDbAFA70XXkBUVUMAXWiOxU5V6gHO4yhXbTFEn7922JZlY7PIjo2Q+BxLkozMzuh8MZdoeadqffZX1fOuyTRWfPlqi7vIYgnUyTmThKe2EZv1LsB5ZUaX3KSArKDv1xPTKS0nexGNZoFckwEfVr6B2PGbMx8LPLYEEEmd95kh8NAKN1wkOPuBehLAtbMtcnLpTsotY6diqWdW4V9BSst0KDMTxZVfeesWD7/7ga9hzNvAWO1MN3aAvDCiQVufb44i4Qfu6fLS7+nxtcDCN2PqPHcANcW0cUhUNB50ajzNwRXN8B92CiY0zkS61CzWeooHOslGp0Acau1CJy8iHGyjzbPS4ui8F2h2TbDUuInOoMqiRjXFvRTxA=',
encrypted_blobdecrypt_key: 'AA9vUokfQ1WXEOArl2DUwY3cxgXGKj9uNEqrJQzUu0hqXIWRu1V+6l1qqxXKPnm9BNscMpm0BMSbxUz++lfV50c1B4akvrzIBH+MUUgNyyPcHR7JBgjEYt0=',
patches: [],
result: 'success'
}
}
var blob = new Blob();
blob.url = exampleData.blob.url;
blob.id = exampleData.blob.id;
blob.key = exampleData.blob.key;
blob.data = exampleData.blob.data;
blob.revision = exampleData.blob.data.revision;
//must be set for self signed certs
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
while(!sjcl.random.isReady()) {
sjcl.random.addEntropy(require('crypto').randomBytes(128).toString('base64')); //add entropy to seed the generator
}
var mockRippleTxt;
var mockRippleTxt2;
var mockAuthSign;
var mockRegister;
var mockBlob;
var mockRename;
var mockUpdate;
var mockRecover;
var mockVerify;
var mockEmail;
var mockProfile;
var mockDelete;
if (!online) {
mockRippleTxt = nock('https://ripple.com')
.persist()
.get('/ripple.txt')
.reply(200, rippleTxtRes, {
'Content-Type': 'text/plain'
});
mockRippleTxt2 = nock('https://' + exampleData.domain)
.persist()
.get('/ripple.txt')
.reply(200, rippleTxtRes, {
'Content-Type': 'text/plain'
});
mockAuthSign = nock('https://auth1.ripple.com')
.persist()
.post('/api/sign')
.reply(200, signRes, {
'Content-Type': 'text/plain'
});
mockRegister = nock('https://id.staging.ripple.com').persist();
mockRegister.filteringPath(/(v1\/user\?signature(.+))/g, 'register/')
.post('/register/')
.reply(200, { result: 'error', message: 'User already exists' }, {
'Content-Type': 'application/json'
});
mockDelete = nock('https://id.staging.ripple.com').persist();
mockDelete.filteringPath(/(v1\/user\/(.+))/g, 'delete/')
.delete('/delete/')
.reply(200, { result: 'success' }, {
'Content-Type': 'application/json'
});
mockBlob = nock('https://id.staging.ripple.com').persist();
mockBlob.get('/v1/authinfo?domain=' + exampleData.domain + '&username=' + exampleData.username.toLowerCase())
.reply(200, JSON.stringify(authInfoRes.body), {
'Content-Type': 'application/json'
});
mockBlob.get('/v1/authinfo?domain=' + exampleData.domain + '&username=' + exampleData.new_username.toLowerCase())
.reply(200, JSON.stringify(authInfoNewUsernameRes.body), {
'Content-Type': 'application/json'
});
mockBlob.filteringPath(/(blob\/.+)/g, 'blob/')
.get('/v1/blob/')
.reply(200, JSON.stringify(blobRes.body), {
'Content-Type': 'application/json'
});
mockRename = nock('https://id.staging.ripple.com/v1/user/').persist();
mockRename.filteringPath(/((.+)\/rename(.+))/g, 'rename/')
.post('rename/')
.reply(200, {result:'success',message:'rename'}, {
'Content-Type': 'application/json'
});
mockUpdate = nock('https://id.staging.ripple.com/v1/user/').persist();
mockUpdate.filteringPath(/((.+)\/update(.+))/g, 'update/')
.post('update/')
.reply(200, {result:'success',message:'updateKeys'}, {
'Content-Type': 'application/json'
});
mockRecover = nock('https://id.staging.ripple.com/').persist();
mockRecover.filteringPath(/((.+)user\/recov\/(.+))/g, 'recov/')
.get('recov/')
.reply(200, recoverRes.body, {
'Content-Type': 'application/json'
});
mockVerify = nock('https://id.staging.ripple.com/v1/user/').persist();
mockVerify.filteringPath(/((.+)\/verify(.+))/g, 'verify/')
.get('verify/')
.reply(200, {result:'error', message:'invalid token'}, {
'Content-Type': 'application/json'
});
mockEmail = nock('https://id.staging.ripple.com/v1/user').persist();
mockEmail.filteringPath(/((.+)\/email(.+))/g, 'email/')
.post('email/')
.reply(200, {result:'success'}, {
'Content-Type': 'application/json'
});
mockProfile = nock('https://id.staging.ripple.com/v1/user').persist();
mockProfile.filteringPath(/((.+)\/profile(.+))/g, 'profile/')
.post('profile/')
.reply(200, {result:'success'}, {
'Content-Type': 'application/json'
});
}
describe('Ripple Txt', function () {
it('should get the content of a ripple.txt file from a given domain', function(done) {
RippleTxt.get(exampleData.domain, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
done();
});
});
it('should get currencies from a ripple.txt file for a given domain', function(done) {
RippleTxt.getCurrencies(exampleData.domain, function(err, currencies) {
assert.ifError(err);
assert(Array.isArray(currencies));
done();
});
});
it('should get the domain from a given url', function() {
var domain = RippleTxt.extractDomain("http://www.example.com");
assert.strictEqual(typeof domain, 'string');
});
});
describe('AuthInfo', function() {
it('should get auth info', function(done) {
AuthInfo.get(exampleData.domain, exampleData.username, function(err, resp) {
assert.ifError(err);
Object.keys(authInfoRes.body).forEach(function(prop) {
assert(resp.hasOwnProperty(prop));
});
done();
});
});
});
describe('VaultClient', function () {
var client = new VaultClient(exampleData.domain);
describe('#initialization', function() {
it('should be initialized with a domain', function() {
var client = new VaultClient({ domain: exampleData.domain });
assert.strictEqual(client.domain, exampleData.domain);
});
it('should default to ripple.com without a domain', function () {
var client = new VaultClient();
assert.strictEqual(client.domain, 'ripple.com');
});
});
describe('#exists', function() {
it('should determine if a username exists on the domain', function(done) {
this.timeout(10000);
client.exists(exampleData.username, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'boolean');
done();
});
});
});
describe('#login', function() {
it('with username and password should retrive the blob, crypt key, and id', function(done) {
this.timeout(10000);
client.login(exampleData.username, exampleData.password, function(err, resp) {
if (online) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert(resp.blob instanceof Blob);
assert.strictEqual(typeof resp.blob.id, 'string');
assert(UInt256.from_json(resp.blob.id).is_valid());
assert.strictEqual(typeof resp.blob.key, 'string');
assert(UInt256.from_json(resp.blob.key).is_valid());
assert.strictEqual(typeof resp.username, 'string');
assert.strictEqual(typeof resp.verified, 'boolean');
} else {
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
}
done();
});
});
});
describe('#relogin', function() {
it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) {
this.timeout(10000);
client.relogin(exampleData.blob.url, exampleData.id, exampleData.crypt, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert(resp.blob instanceof Blob);
done();
});
});
});
describe('#unlock', function() {
it('should access the wallet secret using encryption secret, username and password', function (done) {
this.timeout(10000);
client.unlock(exampleData.username, exampleData.password, exampleData.blob.data.encrypted_secret, function(err, resp) {
if (online) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.keys, 'object');
assert.strictEqual(typeof resp.keys.unlock, 'string');
assert(UInt256.from_json(resp.keys.unlock).is_valid());
} else {
assert.strictEqual(err.toString(), 'CORRUPT: ccm: tag doesn\'t match');
assert.strictEqual(resp, void(0));
}
done();
});
});
});
describe('#loginAndUnlock', function () {
it('should get the decrypted blob and decrypted secret given name and password', function (done) {
this.timeout(10000);
client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) {
if (online) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert(resp.blob instanceof Blob);
assert.strictEqual(typeof resp.blob.id, 'string');
assert(UInt256.from_json(resp.blob.id).is_valid());
assert.strictEqual(typeof resp.blob.key, 'string');
assert(UInt256.from_json(resp.blob.key).is_valid());
assert.strictEqual(typeof resp.unlock, 'string');
assert(UInt256.from_json(resp.unlock).is_valid());
assert.strictEqual(typeof resp.secret, 'string');
assert.strictEqual(typeof resp.username, 'string');
assert.strictEqual(typeof resp.verified, 'boolean');
} else {
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
}
done();
});
});
});
describe('#register', function () {
it('should create a new blob', function (done) {
this.timeout(10000);
var options = {
username : exampleData.username,
password : exampleData.password,
email : exampleData.blob.data.email,
activateLink : exampleData.activateLink
}
client.register(options, function(err, resp) {
//fails, user already exists
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
done();
});
});
});
describe('#deleteBlob', function () {
it('should remove an existing blob', function (done) {
this.timeout(10000);
var options = {
url : exampleData.blob.url,
blob_id : exampleData.blob.id,
username : online ? "" : exampleData.username,
account_id : exampleData.blob.data.account_id,
masterkey : exampleData.masterkey
}
client.deleteBlob(options, function(err, resp) {
if (online) {
//removing the username will result in an error from the server
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
} else {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
}
done();
});
});
});
describe('#updateProfile', function () {
it('should update profile parameters associated with a blob', function (done) {
this.timeout(10000);
var options = {
url : exampleData.blob.url,
blob_id : exampleData.blob.id,
username : exampleData.username,
auth_secret : exampleData.blob.data.auth_secret,
profile : {
city : "San Francisco",
phone : "555-555-5555"
}
}
client.updateProfile(options, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
done();
});
});
});
});
describe('Blob', function () {
var client;
var resp;
client = new VaultClient({ domain: exampleData.domain });
before(function(done) {
if (online) {
this.timeout(10000);
client.login(exampleData.username, exampleData.password, function(err, res) {
resp = res;
blob = res.blob;
done();
});
} else {
mockBlob.filteringPath(/(blob\/.+)/g, 'blob/')
.post('/v1/blob/')
.reply(200, {result:'success'}, {
'Content-Type': 'application/json'
});
done();
}
});
describe('#rename', function () {
it('should change the username of a blob', function (done) {
this.timeout(20000);
var options = {
username : exampleData.username,
new_username : exampleData.new_username,
password : exampleData.password,
masterkey : exampleData.masterkey,
blob : blob
}
client.rename(options, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
assert.strictEqual(typeof resp.message, 'string');
if (online) {
options.username = exampleData.new_username;
options.new_username = exampleData.username;
//change it back
client.rename(options, function(err,resp){
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
assert.strictEqual(typeof resp.message, 'string');
done();
});
} else {
done();
}
});
});
});
describe('#changePassword', function () {
it('should change the password and keys of a blob', function (done) {
this.timeout(10000);
var options = {
username : exampleData.username,
password : exampleData.password,
masterkey : exampleData.masterkey,
blob : blob
}
client.changePassword(options, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
assert.strictEqual(typeof resp.message, 'string');
done();
});
});
});
describe('#recoverBlob', function () {
it('should recover the blob given a username and secret', function (done) {
this.timeout(10000);
var options = {
url : exampleData.blob.url,
username : exampleData.username,
masterkey : exampleData.masterkey,
}
client.recoverBlob(options, function(err, blob) {
assert.ifError(err);
assert(blob instanceof Blob);
done();
});
});
});
describe('#verifyEmail', function () {
it('should verify an email given a username and token', function (done) {
this.timeout(10000);
client.verify(exampleData.username, exampleData.email_token, function(err, resp) {
//result will be error, because of invalid token
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
done();
});
});
});
describe('#resendVerifcationEmail', function () {
it('should resend a verification given options', function (done) {
this.timeout(10000);
var options = {
url : exampleData.blob.url,
id : exampleData.blob.id,
username : exampleData.username,
account_id : exampleData.blob.data.account_id,
email : exampleData.blob.data.email,
activateLink : exampleData.activateLink,
masterkey : exampleData.masterkey
}
client.resendEmail(options, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
assert.strictEqual(typeof resp.result, 'string');
done();
});
});
});
it('#set', function(done) {
this.timeout(10000)
blob.extend('/testObject', {
foo: [],
}, function(err, resp) {
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#extend', function(done) {
this.timeout(10000)
blob.extend('/testObject', {
foobar: 'baz',
}, function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#unset', function(done) {
this.timeout(10000)
blob.unset('/testObject', function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#unshift', function(done) {
this.timeout(10000)
blob.unshift('/testArray', {
name: 'bob',
address: '1234'
}, function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#filter', function(done) {
this.timeout(10000)
blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:'Alice'}, function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#consolidate', function(done) {
this.timeout(10000)
blob.unset('/testArray', function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
blob.consolidate(function(err, resp){
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
});
describe('identity', function() {
it('#identity_set', function (done) {
this.timeout(10000);
blob.identity.set('address', exampleData.unlock, {city:"San Francisco", region:"CA"}, function (err, resp) {
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
it('#identity_get', function () {
var property = blob.identity.get('address', exampleData.unlock);
assert.ifError(property.error);
assert.strictEqual(typeof property.encrypted, 'boolean');
assert.notEqual(typeof property.value, 'undefined');
});
it('#identity_getAll', function () {
var obj = blob.identity.getAll(exampleData.unlock);
assert.strictEqual(typeof obj, 'object');
});
it('#identity_getFullAddress', function () {
var address = blob.identity.getFullAddress(exampleData.unlock);
assert.strictEqual(typeof address, 'string');
});
it('#identity_unset', function (done) {
this.timeout(10000);
blob.identity.unset('name', exampleData.unlock, function (err, resp) {
assert.ifError(err);
assert.strictEqual(resp.result, 'success');
done();
});
});
});
if (!online) {
after(function () {
nock.restore();
});
}
});