├── couchbase ├── mk_bucket.sh ├── chkvars.sh ├── add_track_contract.sh └── mk_indexes.sh ├── systemd ├── state_track_dbwrite@.service └── state_track_api.service ├── README.md ├── scripts └── state_track_dbwrite.pl └── api └── state_track.psgi /couchbase/mk_bucket.sh: -------------------------------------------------------------------------------- 1 | . ./chkvars.sh 2 | 3 | couchbase-cli bucket-create --username $COUCH_USER --password $COUCH_PW --cluster couchbase://localhost --bucket $COUCH_BUCKET --bucket-eviction-policy fullEviction --bucket-type couchbase --bucket-ramsize 10000 4 | 5 | -------------------------------------------------------------------------------- /couchbase/chkvars.sh: -------------------------------------------------------------------------------- 1 | if [ x$COUCH_USER = x ]; then echo "COUCH_USER undefined" 1>&2; exit 1; fi 2 | if [ x$COUCH_PW = x ]; then echo "COUCH_PW undefined" 1>&2; exit 1; fi 3 | if [ x$COUCH_BUCKET = x ]; then 4 | echo "using default COUCH_BUCKET=state_track" 1>&2 5 | COUCH_BUCKET=state_track 6 | export COUCH_BUCKET 7 | fi 8 | -------------------------------------------------------------------------------- /systemd/state_track_dbwrite@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=state_track DB Writer 3 | 4 | [Service] 5 | Type=simple 6 | After=couchbase-server 7 | Environment="STATETRACK_HOME=/opt/eosio_state_track" 8 | EnvironmentFile=/etc/default/state_track_%i 9 | 10 | ExecStart=/usr/bin/perl ${STATETRACK_HOME}/scripts/state_track_dbwrite.pl --network=%i $DBWRITE_OPTS 11 | TimeoutSec=45s 12 | Restart=always 13 | RestartSec=60 14 | User=root 15 | Group=daemon 16 | KillMode=control-group 17 | 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /couchbase/add_track_contract.sh: -------------------------------------------------------------------------------- 1 | . ./chkvars.sh 2 | 3 | if [ $# -ne 5 ]; then 4 | echo "Usage: $0 NEWORK ACCOUNT TYPE TRACKTABLES TRACKTX" 1>&2; 5 | exit 1; 6 | fi 7 | 8 | NETWORK=$1 9 | ACC=$2 10 | TYPE=$3 11 | TRACKTABLES=$4 12 | TRACKTX=$5 13 | 14 | 15 | cbc create contract:${NETWORK}:${ACC} -V \ 16 | '{"type":"contract", "network":"'${NETWORK}'", "account_name":"'${ACC}'", "contract_type":"'${TYPE}'", "track_tables":"'${TRACKTABLES}'", "track_tx":"'${TRACKTX}'"}' \ 17 | -M upsert -u $COUCH_USER -P $COUCH_PW -U couchbase://localhost/$COUCH_BUCKET 18 | -------------------------------------------------------------------------------- /systemd/state_track_api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=State Track API 3 | 4 | [Service] 5 | Type=simple 6 | Environment="STATE_TRACK_CFG=/etc/eosio_state_track/default.pl" 7 | Environment="STATE_TRACK_HOME=/opt/eosio_state_track" 8 | Environment="LISTEN=0.0.0.0:5001" 9 | Environment="WORKERS=6" 10 | EnvironmentFile=-/etc/default/state_track_api 11 | ExecStart=/usr/local/bin/starman --listen ${LISTEN} --workers ${WORKERS} ${STATE_TRACK_HOME}/api/state_track.psgi 12 | TimeoutSec=45s 13 | Restart=always 14 | User=root 15 | Group=daemon 16 | KillMode=control-group 17 | 18 | 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | 23 | 24 | -------------------------------------------------------------------------------- /couchbase/mk_indexes.sh: -------------------------------------------------------------------------------- 1 | . ./chkvars.sh 2 | 3 | alias q="cbc-n1ql -u $COUCH_USER -P $COUCH_PW -U couchbase://localhost/$COUCH_BUCKET" 4 | 5 | q "CREATE INDEX sync_01 ON $COUCH_BUCKET(head_reached) WHERE type = 'sync'" 6 | 7 | q "CREATE INDEX contract_01 ON $COUCH_BUCKET(network,contract_type,account_name) WHERE type = 'contract'" 8 | 9 | 10 | 11 | q "CREATE INDEX tbl_upd_01 ON $COUCH_BUCKET(network,TONUM(block_num)) WHERE type = 'table_upd'" 12 | 13 | q "CREATE INDEX tbl_upd_02 ON $COUCH_BUCKET(network,code,tblname,scope,primary_key,added) WHERE type = 'table_upd'" 14 | 15 | q "CREATE INDEX tbl_upd_03 ON $COUCH_BUCKET(network,contract_type,tblname,scope,added) WHERE type = 'table_upd'" 16 | 17 | q "CREATE INDEX tbl_upd_04 ON $COUCH_BUCKET(network,rowval.owner,added) WHERE type = 'table_upd' AND contract_type='token:dgoods' AND tblname='dgood' AND scope=code" 18 | 19 | q "CREATE INDEX tbl_upd_05 ON $COUCH_BUCKET(network,code,scope,rowval.schema_name) WHERE type = 'table_upd' AND contract_type='token:atomicassets' AND tblname='schemas' USING GSI WITH {'defer_build':true}" 20 | 21 | q "CREATE INDEX tbl_upd_06 ON $COUCH_BUCKET(network,code,scope,rowval.template_id) WHERE type = 'table_upd' AND contract_type='token:atomicassets' AND tblname='templates' USING GSI WITH {'defer_build':true}" 22 | 23 | 24 | 25 | q "CREATE INDEX tbl_row_02 ON $COUCH_BUCKET(network,code,tblname,scope,primary_key) WHERE type = 'table_row'" 26 | 27 | q "CREATE INDEX tbl_row_03 ON $COUCH_BUCKET(network,contract_type,tblname,scope) WHERE type = 'table_row'" 28 | 29 | q "CREATE INDEX tbl_row_04 ON $COUCH_BUCKET(network,rowval.owner) WHERE type = 'table_row' AND contract_type='token:dgoods' AND tblname='dgood' AND scope=code" 30 | 31 | q "CREATE INDEX tbl_row_05 ON $COUCH_BUCKET(network,code,scope,rowval.schema_name) WHERE type = 'table_row' AND contract_type='token:atomicassets' AND tblname='schemas' USING GSI WITH {'defer_build':true}" 32 | 33 | q "CREATE INDEX tbl_row_06 ON $COUCH_BUCKET(network,code,scope,rowval.template_id) WHERE type = 'table_row' AND contract_type='token:atomicassets' AND tblname='templates' USING GSI WITH {'defer_build':true}" 34 | 35 | 36 | q "BUILD INDEX ON $COUCH_BUCKET ('tbl_upd_05', 'tbl_upd_06', 'tbl_row_05', 'tbl_row_06')" 37 | 38 | 39 | 40 | q "CREATE INDEX tx_01 ON $COUCH_BUCKET(network, DISTINCT ARRAY acc FOR acc IN tx_accounts END, TONUM(trace.action_traces[0].receipt.global_sequence) DESC) WHERE type = 'transaction'" 41 | 42 | q "CREATE INDEX tx_02 ON $COUCH_BUCKET(network, DISTINCT ARRAY acc FOR acc IN tx_accounts END, TONUM(block_num) DESC, TONUM(trace.action_traces[0].receipt.global_sequence) DESC) WHERE type = 'transaction'" 43 | 44 | 45 | q "CREATE INDEX tx_upd_01 ON $COUCH_BUCKET(network,TONUM(block_num)) WHERE type = 'transaction_upd'" 46 | 47 | q "CREATE INDEX tx_upd_02 ON $COUCH_BUCKET(network, DISTINCT ARRAY acc FOR acc IN tx_accounts END, TONUM(block_num),TONUM(trace.action_traces[0].receipt.global_sequence)) WHERE type = 'transaction_upd'" 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EOSIO State Track API 2 | 3 | The API is storing table rows and transaction traces for a number of 4 | smart contracts and provides a way for searching and retrieving the 5 | data. 6 | 7 | 8 | The API accepts HTTP GET and POST requests, so it's up to you to 9 | choose the right one. Many SSE libraries are not allowing to send 10 | anything but GET requests. 11 | 12 | The API output is a stream of [Server-Sent 13 | Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) 14 | (SSE). It allows transporting large chunks of data without much 15 | overhead, and there's support in browsers and javascript libraries. 16 | 17 | The output is a number of events, such as `row`, `rowupd`, `tx`, 18 | followed by `end` event. 19 | 20 | Presence of `end` event indicates that the whole output has been 21 | delivered. The event also contains the total count of events preceding 22 | it. 23 | 24 | Data field in each event is a JSON object. 25 | 26 | `block_num` and `block_timestamp` attributes in most of the output are 27 | informational and indicate the latest block that updated a specific data 28 | element (in case of `contracts` request, they indicate the moment when 29 | the contract was added to the database). 30 | 31 | 32 | 33 | ## Networks 34 | 35 | * `https://HOST/strack/networks` lists the EOSIO blockchains known to 36 | the API, their actual head block status and irreversible block number. 37 | 38 | 39 | ## NFT Assets 40 | 41 | * `https://HOST/strack/tokens?network=NETWORK&account=ACCOUNT` lists 42 | all NFT assets belonging to the account. The `rowval` field in the 43 | data JSON represents a table row in corresponding smart contract. 44 | `contract_type` field indicates the type of a smart contract, such 45 | as "token:simpleassets" or "token:dgoods" or "token:atomicassets", 46 | and the client software has to interpret the row contents according 47 | to the type of a contract. 48 | 49 | 50 | For AtomicAssets objects, the client needs to retrieve the encoding 51 | schema in order to be able to display them. The schemas are contained 52 | in "schemas" table, and collection name is the scope: 53 | 54 | `https://HOST/strack/table_rows?network=NETWORK&code=ACCOUNT&table=schemas&scope=COLLNAME` 55 | 56 | 57 | ## Contracts 58 | 59 | * `https://HOST/strack/contracts?network=NETWORK` lists smart contract 60 | accounts that are being tracked by the API. The following attributes 61 | are of importance: 62 | 63 | * `account_name` specifies the EOSIO account name; 64 | 65 | * `contract_type` specifies the type of contract. It is used in other 66 | queries. 67 | 68 | * `track_tables`: if present and set to `true`, contract table 69 | contents are tracked and returned by the API. 70 | 71 | * `track_tx`: if present and set to `true`, transaction history of 72 | this account is stored and retyurned by the API. 73 | 74 | 75 | ## Contract tables 76 | 77 | 78 | * `https://HOST/strack/contract_tables?network=NETWORK&code=ACCOUNT` 79 | lists names of tables in the smart contract, in `tblname` attribute. 80 | 81 | 82 | * `https://HOST/strack/table_scopes?network=NETWORK&code=ACCOUNT&table=TABLE` 83 | lists the scopes of a table in `scope` attribute. 84 | 85 | 86 | * `https://HOST//strack/table_rows?network=NETWORK&code=ACCOUNT&table=TABLE&scope=SCOPE` 87 | lists table rows. First come rows that are updated by irreversible 88 | transactions, in `row` events in undefined order. Then `rowupd` events 89 | follow in chronological order. They have an additional attribute 90 | `added` which is set to `true` if the row is added or modified, and 91 | `false` of a row is deleted. Attributes in every event are as follows: 92 | 93 | * `primary_key` is the table primary index value in numeric notation; 94 | 95 | * `rowval` is the value of a row as a JSON object. 96 | 97 | 98 | * `https://HOST//strack/table_row_by_pk?network=NETWORK&code=ACCOUNT&table=TABLE&scope=SCOPE&pk=N' 99 | returns a single or none `row` event comprising of one table row value. 100 | 101 | 102 | * `https://HOST//strack/table_rows_by_scope?network=NETWORK&table=TABLE&scope=SCOPE&contract_type=CTYPE` 103 | allows searching for a specific scope in tables across multiple smart 104 | contracts. This is useful if you need to find all NFT tokens belonging 105 | to an account. The result is in the same format as for `table_rows` 106 | request. 107 | 108 | 109 | ## Account history 110 | 111 | `https://HOST//strack/account_history?network=NETWORK&account=ACCOUNT` 112 | returns transaction traces relevant for the account in reverse 113 | order. Without additional parameters, it returns 100 latest 114 | transactions. 115 | 116 | Additional HTTP request parameters: 117 | 118 | * `maxrows` indicates the maximum entries to return. The output length 119 | may potentially be double of this number if there are so many 120 | speculative transactions. 121 | 122 | * `start_block` indicates the lowest block number in the output. 123 | 124 | * `end_block` indicates the highest block in the output. 125 | 126 | 127 | The result is a number of `tx` events with the following attributes: 128 | 129 | * `id` is a concatenation of the word 'tx', network name, and 130 | transaction ID, separated by colons. 131 | 132 | * `block_num` and `block_timestamp` indicate the block of transaction. 133 | 134 | * `irreversible`: `true` or `false` indicates whether the transaction is 135 | found in an irreversible or speculative block. 136 | 137 | * `trace` contains a full transaction trace object as returned by 138 | Chronicle. 139 | 140 | 141 | 142 | # Data collection 143 | 144 | There are two ways how accounts are marked for tracking: by manual 145 | insertion in the database, and by having an entry in 146 | [`tokenconfigs`](https://github.com/eosio-standards-wg/tokenconfigs) 147 | table. 148 | 149 | 150 | # Public API endpoints 151 | 152 | A list of public API endpoints is served by IPFS. It shares the same 153 | endpoints JSON as 154 | [LightAPI](https://github.com/cc32d9/eosio_light_api), listing the 155 | endpoints under "state-track-endpoints" section. The list is available 156 | with the following links: 157 | 158 | * https://endpoints.light.xeos.me/endpoints.json (served by Cloudflare) 159 | 160 | * https://ipfs.io/ipns/QmTuBHRokSuiLBiqE1HySfK1BFiT2pmuDTuJKXNganE52N/endpoints.json 161 | 162 | 163 | 164 | # Project sponsors 165 | 166 | * [GetScatter](https://get-scatter.com/): funding the software 167 | development; 168 | 169 | * [EOS Amsterdam](https://eosamsterdam.net/): hosting of public 170 | endpoints. 171 | 172 | 173 | 174 | 175 | # Souce code, license and copyright 176 | 177 | Source code repository: https://github.com/GetScatter/EOSIO-State-Track 178 | 179 | Copyright (c) 2019 GetScatter Ltd. 180 | 181 | Permission is hereby granted, free of charge, to any person obtaining a copy 182 | of this software and associated documentation files (the "Software"), to deal 183 | in the Software without restriction, including without limitation the rights 184 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 185 | copies of the Software, and to permit persons to whom the Software is 186 | furnished to do so, subject to the following conditions: 187 | 188 | The above copyright notice and this permission notice shall be included in all 189 | copies or substantial portions of the Software. 190 | 191 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 192 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 193 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 194 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 195 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 196 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 197 | SOFTWARE. 198 | 199 | -------------------------------------------------------------------------------- /scripts/state_track_dbwrite.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use JSON; 4 | use Getopt::Long; 5 | use Digest::SHA qw(sha256_hex); 6 | use Time::HiRes qw (time sleep); 7 | 8 | use Couchbase::Bucket; 9 | use Couchbase::Document; 10 | 11 | use Net::WebSocket::Server; 12 | use Protocol::WebSocket::Frame; 13 | 14 | $Protocol::WebSocket::Frame::MAX_PAYLOAD_SIZE = 100*1024*1024; 15 | $Protocol::WebSocket::Frame::MAX_FRAGMENTS_AMOUNT = 102400; 16 | 17 | $| = 1; 18 | 19 | 20 | my $network; 21 | my $track_tokenconfigs = 1; 22 | 23 | my $port = 8100; 24 | my $ack_every = 4; 25 | 26 | my $dbhost = '127.0.0.1'; 27 | my $bucket = 'state_track'; 28 | my $dbuser = 'Administrator'; 29 | my $dbpw = 'password'; 30 | 31 | 32 | my $ok = GetOptions 33 | ( 34 | 'network=s' => \$network, 35 | 'tkcfg' => \$track_tokenconfigs, 36 | 'port=i' => \$port, 37 | 'ack=i' => \$ack_every, 38 | 'dbhost=s' => \$dbhost, 39 | 'bucket=s' => \$bucket, 40 | 'dbuser=s' => \$dbuser, 41 | 'dbpw=s' => \$dbpw, 42 | ); 43 | 44 | 45 | if( not $ok or scalar(@ARGV) > 0 or not $network ) 46 | { 47 | print STDERR "Usage: $0 --network=eos [options...]\n", 48 | "The utility opens a WS port for Chronicle to send data to.\n", 49 | "Options:\n", 50 | " --port=N \[$port\] TCP port to listen to websocket connection\n", 51 | " --ack=N \[$ack_every\] Send acknowledgements every N blocks\n", 52 | " --network=NAME name of EOS network\n", 53 | " --notkcfg skip looking up tokenconfigs table\n", 54 | " --dbhost=HOST \[$dbhost] Couchbase host\n", 55 | " --bucket=NAME \[$bucket] Couchbase bucket\n", 56 | " --dbuser=USER \[$dbuser\] Couchbase user\n", 57 | " --dbpw=PW \[$dbpw\] Couchbase password\n"; 58 | exit 1; 59 | } 60 | 61 | my $cb = Couchbase::Bucket->new('couchbase://' . $dbhost . '/' . $bucket, 62 | {'username' => $dbuser, 'password' => $dbpw}); 63 | 64 | my $json = JSON->new->canonical; 65 | 66 | my $confirmed_block = 0; 67 | my $unconfirmed_block = 0; 68 | my $irreversible = 0; 69 | my $head_reached = 0; 70 | 71 | my %acc_store_deltas; 72 | my %acc_store_traces; 73 | my %acc_contract_type; 74 | 75 | # set them to nonzero to process all leftovers on first run 76 | my $has_upd_tables = 1; 77 | my $has_upd_tx = 1; 78 | 79 | my $contracts_last_fetched = 0; 80 | refresh_contracts(); 81 | 82 | { 83 | my $doc = Couchbase::Document->new('sync:' . $network); 84 | $cb->get($doc); 85 | if( $doc->is_ok() ) 86 | { 87 | $confirmed_block = $doc->value()->{'block_num'}; 88 | $unconfirmed_block = $confirmed_block; 89 | $irreversible = $doc->value()->{'irreversible'}; 90 | $head_reached = $doc->value()->{'head_reached'}; 91 | print STDERR "Last confirmed block: $confirmed_block, Irreversible: $irreversible\n"; 92 | } 93 | } 94 | 95 | 96 | Net::WebSocket::Server->new( 97 | listen => $port, 98 | on_connect => sub { 99 | my ($serv, $conn) = @_; 100 | $conn->on( 101 | 'binary' => sub { 102 | my ($conn, $msg) = @_; 103 | my ($msgtype, $opts, $js) = unpack('VVa*', $msg); 104 | my $data = eval {$json->decode($js)}; 105 | if( $@ ) 106 | { 107 | print STDERR $@, "\n\n"; 108 | print STDERR $js, "\n"; 109 | exit; 110 | } 111 | 112 | my $ack = process_data($msgtype, $data, \$js); 113 | if( $ack > 0 ) 114 | { 115 | $conn->send_binary(sprintf("%d", $ack)); 116 | print STDERR "ack $ack\n"; 117 | } 118 | }, 119 | 'disconnect' => sub { 120 | print STDERR "Disconnected\n"; 121 | }, 122 | 123 | ); 124 | }, 125 | )->start; 126 | 127 | 128 | sub process_data 129 | { 130 | my $msgtype = shift; 131 | my $data = shift; 132 | my $jsptr = shift; 133 | 134 | if( $msgtype == 1001 ) # CHRONICLE_MSGTYPE_FORK 135 | { 136 | if( $data->{'last_irreversible'} > 0 ) 137 | { 138 | $irreversible = $data->{'last_irreversible'}; 139 | } 140 | 141 | my $block_num = $data->{'block_num'}; 142 | print STDERR "fork at $block_num; irreversible: $irreversible\n"; 143 | 144 | $cb->query_slurp('DELETE FROM ' . $bucket . ' WHERE type=\'table_upd\' ' . 145 | 'AND network=\'' . $network . '\' AND TONUM(block_num)>=' . $block_num, 146 | {}, {'scan_consistency' => '"request_plus"'}); 147 | 148 | $cb->query_slurp('DELETE FROM ' . $bucket . ' WHERE type=\'transaction_upd\' ' . 149 | 'AND network=\'' . $network . '\' AND TONUM(block_num)>=' . $block_num, 150 | {}, {'scan_consistency' => '"request_plus"'}); 151 | 152 | $confirmed_block = $block_num-1; 153 | $unconfirmed_block = $block_num-1; 154 | return $confirmed_block; 155 | } 156 | elsif( $msgtype == 1007 ) # CHRONICLE_MSGTYPE_TBL_ROW 157 | { 158 | my $kvo = $data->{'kvo'}; 159 | if( ref($kvo->{'value'}) eq 'HASH' ) 160 | { 161 | my $contract = $kvo->{'code'}; 162 | my $table = $kvo->{'table'}; 163 | my $block_num = $data->{'block_num'}; 164 | my $block_time = $data->{'block_timestamp'}; 165 | 166 | if( $track_tokenconfigs and 167 | $table eq 'tokenconfigs' and defined($kvo->{'value'}{'standard'}) ) 168 | { 169 | my $type = 'token:' . $kvo->{'value'}{'standard'}; 170 | $acc_store_deltas{$contract} = 1; 171 | $acc_contract_type{$contract} = $type; 172 | 173 | my $doc = Couchbase::Document->new( 174 | 'contract:' . $network . ':' . $contract, { 175 | 'type' => 'contract', 176 | 'network' => $network, 177 | 'account_name' => $contract, 178 | 'contract_type' => $type, 179 | 'track_tables' => 'true', 180 | 'block_timestamp' => $block_time, 181 | 'block_num' => $block_num, 182 | }); 183 | $cb->upsert($doc); 184 | while( not $doc->is_ok ) 185 | { 186 | print STDERR ("Could not store document: " . $doc->errstr); 187 | sleep 10; 188 | $cb->upsert($doc); 189 | } 190 | print STDERR '.'; 191 | } 192 | 193 | if( $acc_store_deltas{$contract} ) 194 | { 195 | my $rowid = sha256_hex( 196 | join(':', $network, $contract, $table, $kvo->{'scope'}, $kvo->{'primary_key'})); 197 | 198 | my $type = $acc_contract_type{$contract}; 199 | $type = 'unnclassified' unless defined($type); 200 | 201 | if( $block_num > $irreversible and $irreversible > 0 ) 202 | { 203 | my $id = join(':', 'table_upd', $block_num, $rowid, $data->{'added'}); 204 | 205 | my $doc = Couchbase::Document->new( 206 | $id, 207 | { 208 | 'type' => 'table_upd', 209 | 'contract_type' => $type, 210 | 'rowid' => $rowid, 211 | 'network' => $network, 212 | 'code' => $contract, 213 | 'tblname' => $table, 214 | 'added' => $data->{'added'}, 215 | 'scope' => $kvo->{'scope'}, 216 | 'primary_key' => $kvo->{'primary_key'}, 217 | 'rowval' => $kvo->{'value'}, 218 | 'block_timestamp' => $block_time, 219 | 'block_num' => $block_num, 220 | 'block_num_x' => $block_num * 10 + ($data->{'added'} eq 'true' ? 1:0), 221 | }); 222 | $cb->insert($doc); 223 | while( not $doc->is_ok ) 224 | { 225 | print STDERR ("Could not store document: " . $doc->errstr); 226 | sleep 10; 227 | $cb->insert($doc); 228 | } 229 | $has_upd_tables = $block_num; 230 | print STDERR '!'; 231 | } 232 | else 233 | { 234 | my $id = join(':', 'table_row', $rowid); 235 | 236 | if( $data->{'added'} eq 'true' ) 237 | { 238 | my $doc = Couchbase::Document->new( 239 | $id, 240 | { 241 | 'type' => 'table_row', 242 | 'contract_type' => $type, 243 | 'network' => $network, 244 | 'code' => $contract, 245 | 'tblname' => $table, 246 | 'scope' => $kvo->{'scope'}, 247 | 'primary_key' => $kvo->{'primary_key'}, 248 | 'rowval' => $kvo->{'value'}, 249 | 'block_timestamp' => $block_time, 250 | 'block_num' => $block_num, 251 | }); 252 | $cb->upsert($doc); 253 | while( not $doc->is_ok ) 254 | { 255 | print STDERR ("Could not store document: " . $doc->errstr); 256 | sleep 10; 257 | $cb->upsert($doc); 258 | } 259 | print STDERR '@'; 260 | } 261 | else 262 | { 263 | my $doc = Couchbase::Document->new($id); 264 | $cb->remove($doc); 265 | while( not $doc->is_ok and not $doc->is_not_found ) 266 | { 267 | print STDERR ("Could not remove document: " . $doc->errstr); 268 | sleep 10; 269 | $cb->remove($doc); 270 | } 271 | print STDERR '~'; 272 | } 273 | } 274 | } 275 | } 276 | } 277 | elsif( $msgtype == 1003 ) # CHRONICLE_MSGTYPE_TX_TRACE 278 | { 279 | my $trace = $data->{'trace'}; 280 | if( $trace->{'status'} eq 'executed' ) 281 | { 282 | my %accounts; 283 | foreach my $atrace ( @{$trace->{'action_traces'}} ) 284 | { 285 | my $act = $atrace->{'act'}; 286 | $accounts{$atrace->{'receipt'}{'receiver'}} = 1; 287 | $accounts{$act->{'account'}} = 1; 288 | } 289 | 290 | my %accounts_matched; 291 | foreach my $acc (keys %accounts) 292 | { 293 | if( $acc_store_traces{$acc} ) 294 | { 295 | $accounts_matched{$acc} = 1; 296 | } 297 | } 298 | 299 | if( scalar(keys %accounts_matched) > 0 ) 300 | { 301 | $data->{'type'} = 'transaction_upd'; 302 | $data->{'network'} = $network; 303 | $data->{'tx_accounts'} = [sort keys %accounts_matched]; 304 | 305 | my $doc = Couchbase::Document->new( 306 | 'tx:' . $network . ':' . $trace->{'id'}, $data); 307 | $cb->insert($doc); 308 | while( not $doc->is_ok ) 309 | { 310 | print STDERR ("Could not store document: " . $doc->errstr); 311 | sleep 10; 312 | $cb->insert($doc); 313 | } 314 | $has_upd_tx = $data->{'block_num'}; 315 | print STDERR '*'; 316 | } 317 | } 318 | } 319 | elsif( $msgtype == 1009 ) # CHRONICLE_MSGTYPE_RCVR_PAUSE 320 | { 321 | refresh_contracts(); 322 | if( $unconfirmed_block > $confirmed_block ) 323 | { 324 | $confirmed_block = $unconfirmed_block; 325 | return $confirmed_block; 326 | } 327 | } 328 | elsif( $msgtype == 1010 ) # CHRONICLE_MSGTYPE_BLOCK_COMPLETED 329 | { 330 | refresh_contracts(); 331 | my $block_num = $data->{'block_num'}; 332 | my $block_time = $data->{'block_timestamp'}; 333 | my $last_irreversible = $data->{'last_irreversible'}; 334 | 335 | if( $block_num > $unconfirmed_block+1 ) 336 | { 337 | printf STDERR ("WARNING: missing blocks %d to %d\n", $unconfirmed_block+1, $block_num-1); 338 | } 339 | 340 | if( $block_num <= $last_irreversible or $last_irreversible > $irreversible ) 341 | { 342 | if( $has_upd_tables > 0 ) 343 | { 344 | ## process updates 345 | my $rv = $cb->query_iterator 346 | ('SELECT META().id,* FROM ' . $bucket . ' WHERE type=\'table_upd\' ' . 347 | 'AND network=\'' . $network . '\' AND TONUM(block_num)<=' . $last_irreversible . 348 | ' ORDER BY TONUM(block_num_x)', 349 | {}, {'scan_consistency' => '"request_plus"'}); 350 | 351 | while((my $row = $rv->next)) 352 | { 353 | my $obj = $row->{$bucket}; 354 | my $tbl_id = join(':', 'table_row', $obj->{'rowid'}); 355 | 356 | if( $obj->{'added'} eq 'true' ) 357 | { 358 | delete $obj->{'added'}; 359 | delete $obj->{'rowid'}; 360 | delete $obj->{'block_num_x'}; 361 | $obj->{'type'} = 'table_row'; 362 | 363 | my $doc = Couchbase::Document->new($tbl_id, $obj); 364 | $cb->upsert($doc); 365 | while( not $doc->is_ok ) 366 | { 367 | print STDERR ("Could not store document: " . $doc->errstr); 368 | sleep 10; 369 | $cb->upsert($doc); 370 | } 371 | print STDERR '+'; 372 | } 373 | else 374 | { 375 | my $doc = Couchbase::Document->new($tbl_id); 376 | $cb->remove($doc); 377 | while( not $doc->is_ok and not $doc->is_not_found ) 378 | { 379 | print STDERR ("Could not remove document: " . $doc->errstr); 380 | sleep 10; 381 | $cb->remove($doc); 382 | } 383 | print STDERR '-'; 384 | } 385 | 386 | { 387 | my $doc = Couchbase::Document->new($row->{'id'}); 388 | $cb->remove($doc); 389 | while ( not $doc->is_ok ) 390 | { 391 | print STDERR ("Could not remove document: " . $doc->errstr); 392 | sleep 10; 393 | $cb->remove($doc); 394 | } 395 | } 396 | } 397 | 398 | if( $has_upd_tables <= $last_irreversible ) 399 | { 400 | $has_upd_tables = 0; 401 | } 402 | } 403 | 404 | if( $has_upd_tx > 0 ) 405 | { 406 | $cb->query_slurp('UPDATE ' . $bucket . ' SET type=\'transaction\' WHERE type=\'transaction_upd\' ' . 407 | 'AND network=\'' . $network . '\' AND TONUM(block_num)<=' . $last_irreversible, 408 | {}, {'scan_consistency' => '"request_plus"'}); 409 | 410 | if( $has_upd_tx <= $last_irreversible ) 411 | { 412 | $has_upd_tx= 0; 413 | } 414 | } 415 | 416 | $irreversible = $last_irreversible; 417 | } 418 | 419 | if( not $head_reached and $block_num >= $last_irreversible ) 420 | { 421 | $head_reached = 1; 422 | } 423 | 424 | $unconfirmed_block = $block_num; 425 | if( $unconfirmed_block - $confirmed_block >= $ack_every ) 426 | { 427 | my $doc = Couchbase::Document->new( 428 | 'sync:' . $network, { 429 | 'type' => 'sync', 430 | 'network' => $network, 431 | 'block_num' => $block_num, 432 | 'block_time' => $block_time, 433 | 'irreversible' => $last_irreversible, 434 | 'head_reached' => $head_reached, 435 | }); 436 | $cb->upsert($doc); 437 | while( not $doc->is_ok ) 438 | { 439 | print STDERR ("Could not store document: " . $doc->errstr); 440 | sleep 10; 441 | $cb->upsert($doc); 442 | } 443 | 444 | $confirmed_block = $unconfirmed_block; 445 | return $confirmed_block; 446 | } 447 | } 448 | return 0; 449 | } 450 | 451 | 452 | 453 | sub refresh_contracts 454 | { 455 | if( time() - $contracts_last_fetched < 10 ) 456 | { 457 | return; 458 | } 459 | 460 | %acc_store_deltas = (); 461 | %acc_store_traces = (); 462 | %acc_contract_type = (); 463 | 464 | my $rv = $cb->query_iterator('SELECT * FROM ' . $bucket . ' WHERE type=\'contract\' ' . 465 | 'AND network=\'' . $network . '\''); 466 | 467 | while((my $row = $rv->next)) 468 | { 469 | my $acc = $row->{$bucket}{'account_name'}; 470 | next unless defined $acc; 471 | 472 | if( defined($row->{$bucket}{'track_tables'}) and $row->{$bucket}{'track_tables'} eq 'true' ) 473 | { 474 | $acc_store_deltas{$acc} = 1; 475 | } 476 | if( defined($row->{$bucket}{'track_tx'}) and $row->{$bucket}{'track_tx'} eq 'true' ) 477 | { 478 | $acc_store_traces{$acc} = 1; 479 | } 480 | if( defined($row->{$bucket}{'contract_type'}) ) 481 | { 482 | $acc_contract_type{$acc} = $row->{$bucket}{'contract_type'}; 483 | } 484 | } 485 | $contracts_last_fetched = time(); 486 | } 487 | -------------------------------------------------------------------------------- /api/state_track.psgi: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use JSON; 4 | use Plack::Builder; 5 | use Plack::Request; 6 | use Couchbase::Bucket; 7 | use Time::HiRes qw(time); 8 | use DateTime; 9 | use DateTime::Format::ISO8601; 10 | 11 | BEGIN { 12 | if( not defined($ENV{'STATE_TRACK_CFG'}) ) 13 | { 14 | die('missing STATE_TRACK_CFG environment variable'); 15 | } 16 | 17 | if( not -r $ENV{'STATE_TRACK_CFG'} ) 18 | { 19 | die('Cannot access ' . $ENV{'STATE_TRACK_CFG'}); 20 | } 21 | 22 | $CFG::dbhost = '127.0.0.1'; 23 | $CFG::bucket = 'state_track'; 24 | $CFG::dbuser = 'Administrator'; 25 | $CFG::dbpw = 'password'; 26 | $CFG::apiprefix = '/strack/'; 27 | 28 | do $ENV{'STATE_TRACK_CFG'}; 29 | die($@) if($@); 30 | die($!) if($!); 31 | }; 32 | 33 | 34 | my $json = JSON->new()->canonical(); 35 | 36 | 37 | sub get_writer 38 | { 39 | my $responder = shift; 40 | return $responder-> 41 | ( 42 | [ 43 | 200, 44 | [ 45 | 'Content-Type' => 'text/event-stream; charset=UTF-8', 46 | 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', 47 | ] 48 | ] 49 | ); 50 | } 51 | 52 | 53 | sub send_event 54 | { 55 | my $writer = shift; 56 | my $event = shift; 57 | 58 | my @lines; 59 | while( scalar(@{$event}) > 0 ) 60 | { 61 | push(@lines, shift(@{$event}) . ': ' . shift(@{$event})); 62 | } 63 | 64 | $writer->write(join("\x0d\x0a", @lines) . "\x0d\x0a\x0d\x0a"); 65 | } 66 | 67 | sub iterate_and_push 68 | { 69 | my $cb = shift; 70 | my @queries = @_; 71 | 72 | return sub { 73 | my $responder = shift; 74 | my $writer = get_writer($responder); 75 | my $count = 0; 76 | 77 | foreach my $q (@queries) 78 | { 79 | my $eventtype = shift(@{$q}); 80 | # print STDERR join('####', @{$q}), "\n"; 81 | my $rv = $cb->query_iterator(@{$q}); 82 | 83 | while( (my $row = $rv->next()) ) 84 | { 85 | my $event = ['event', $eventtype]; 86 | if( defined($row->{'id'}) ) 87 | { 88 | push(@{$event}, 'id', $row->{'id'}); 89 | } 90 | 91 | push(@{$event}, 'data', $json->encode({%{$row}})); 92 | send_event($writer, $event); 93 | $count++; 94 | } 95 | } 96 | 97 | send_event($writer, ['event', 'end', 'data', $json->encode({'count' => $count})]); 98 | $writer->close(); 99 | }; 100 | } 101 | 102 | 103 | sub push_one_or_nothing 104 | { 105 | my $val = shift; 106 | 107 | return sub { 108 | my $responder = shift; 109 | my $writer = get_writer($responder); 110 | my $count = 0; 111 | if( defined($val) ) 112 | { 113 | send_event($writer, ['event', 'row', 'data', $json->encode({%{$val}})]); 114 | $count++; 115 | } 116 | send_event($writer, ['event', 'end', 'data', $json->encode({'count' => $count})]); 117 | $writer->close(); 118 | } 119 | } 120 | 121 | 122 | sub error 123 | { 124 | my $req = shift; 125 | my $msg = shift; 126 | my $res = $req->new_response(400); 127 | $res->content_type('text/plain'); 128 | $res->body($msg . "\x0d\x0a"); 129 | return $res->finalize; 130 | } 131 | 132 | 133 | 134 | my $cb = Couchbase::Bucket->new('couchbase://' . $CFG::dbhost . '/' . $CFG::bucket, 135 | {'username' => $CFG::dbuser, 'password' => $CFG::dbpw}); 136 | 137 | 138 | 139 | 140 | my $builder = Plack::Builder->new; 141 | 142 | $builder->mount 143 | ($CFG::apiprefix . 'status' => sub { 144 | my $env = shift; 145 | my $req = Plack::Request->new($env); 146 | 147 | my $rv = $cb->query_iterator 148 | ('SELECT block_time,network ' . 149 | 'FROM ' . $CFG::bucket . ' WHERE type=\'sync\' AND head_reached=1'); 150 | my $max = 0; 151 | my $network = ''; 152 | 153 | while( (my $row = $rv->next()) ) 154 | { 155 | my $bt = DateTime::Format::ISO8601->parse_datetime($row->{'block_time'}); 156 | $bt->set_time_zone('UTC'); 157 | 158 | my $now = DateTime->from_epoch(epoch => time(), 'time_zone' => 'UTC'); 159 | my $diff = $now->subtract_datetime_absolute($bt)->in_units('nanoseconds')/1.0e9; 160 | if( $max < $diff ) 161 | { 162 | $max = $diff; 163 | $network = $row->{'network'}; 164 | } 165 | } 166 | 167 | if( $max > 120 ) 168 | { 169 | my $res = $req->new_response(503); 170 | $res->content_type('text/plain'); 171 | $res->body('OUT_OF_SYNC ' . $max . ' ' . $network . "\x0d\x0a"); 172 | return $res->finalize; 173 | } 174 | else 175 | { 176 | my $res = $req->new_response(200); 177 | $res->content_type('text/plain'); 178 | $res->body('OK ' . $max . "\x0d\x0a"); 179 | return $res->finalize; 180 | } 181 | }); 182 | 183 | 184 | $builder->mount 185 | ($CFG::apiprefix . 'networks' => sub { 186 | my $env = shift; 187 | my $req = Plack::Request->new($env); 188 | 189 | return iterate_and_push 190 | ($cb, 191 | ['row', 'SELECT META().id,block_num,block_time,irreversible,network ' . 192 | 'FROM ' . $CFG::bucket . ' WHERE type=\'sync\' AND head_reached=1']); 193 | }); 194 | 195 | 196 | $builder->mount 197 | ($CFG::apiprefix . 'contracts' => sub { 198 | my $env = shift; 199 | my $req = Plack::Request->new($env); 200 | my $p = $req->parameters(); 201 | my $network = $p->{'network'}; 202 | return(error($req, "'network' is not specified")) unless defined($network); 203 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 204 | 205 | my $ctype_filter = ''; 206 | my $ctype = $p->{'contract_type'}; 207 | if( defined($ctype) ) 208 | { 209 | return(error($req, "invalid contract_type")) unless ($ctype =~ /^[a-z0-9:_]+$/); 210 | $ctype_filter = ' AND contract_type=\'' . $ctype . '\' '; 211 | } 212 | 213 | return iterate_and_push 214 | ($cb, 215 | ['row', 216 | 'SELECT META().id, account_name, contract_type, track_tables, ' . 217 | 'track_tx, block_timestamp, block_num ' . 218 | 'FROM ' . $CFG::bucket . ' WHERE type=\'contract\' AND network=\'' . $network . '\'' . 219 | $ctype_filter]); 220 | }); 221 | 222 | 223 | $builder->mount 224 | ($CFG::apiprefix . 'contract_tables' => sub { 225 | my $env = shift; 226 | my $req = Plack::Request->new($env); 227 | my $p = $req->parameters(); 228 | my $network = $p->{'network'}; 229 | return(error($req, "'network' is not specified")) unless defined($network); 230 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 231 | 232 | my $code = $p->{'code'}; 233 | return(error($req, "'code' is not specified")) unless defined($code); 234 | return(error($req, "invalid code")) unless ($code =~ /^[1-5a-z.]{1,13}$/); 235 | 236 | return iterate_and_push 237 | ($cb, 238 | ['row', 239 | 'SELECT distinct tblname ' . 240 | 'FROM ' . $CFG::bucket . ' WHERE (type=\'table_row\' OR type=\'table_upd\') ' . 241 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\'']); 242 | }); 243 | 244 | 245 | $builder->mount 246 | ($CFG::apiprefix . 'table_scopes' => sub { 247 | my $env = shift; 248 | my $req = Plack::Request->new($env); 249 | my $p = $req->parameters(); 250 | my $network = $p->{'network'}; 251 | return(error($req, "'network' is not specified")) unless defined($network); 252 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 253 | 254 | my $code = $p->{'code'}; 255 | return(error($req, "'code' is not specified")) unless defined($code); 256 | return(error($req, "invalid code")) unless ($code =~ /^[1-5a-z.]{1,13}$/); 257 | 258 | my $table = $p->{'table'}; 259 | return(error($req, "'table' is not specified")) unless defined($table); 260 | return(error($req, "invalid table")) unless ($table =~ /^[1-5a-z.]{1,13}$/); 261 | 262 | return iterate_and_push 263 | ($cb, 264 | ['row', 265 | 'SELECT distinct scope ' . 266 | 'FROM ' . $CFG::bucket . ' WHERE (type=\'table_row\' OR type=\'table_upd\') ' . 267 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\' ' . 268 | ' AND tblname=\'' . $table . '\'']); 269 | }); 270 | 271 | 272 | $builder->mount 273 | ($CFG::apiprefix . 'table_rows' => sub { 274 | my $env = shift; 275 | my $req = Plack::Request->new($env); 276 | my $p = $req->parameters(); 277 | my $network = $p->{'network'}; 278 | return(error($req, "'network' is not specified")) unless defined($network); 279 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 280 | 281 | my $code = $p->{'code'}; 282 | return(error($req, "'code' is not specified")) unless defined($code); 283 | return(error($req, "invalid code")) unless ($code =~ /^[1-5a-z.]{1,13}$/); 284 | 285 | my $table = $p->{'table'}; 286 | return(error($req, "'table' is not specified")) unless defined($table); 287 | return(error($req, "invalid table")) unless ($table =~ /^[1-5a-z.]{1,13}$/); 288 | 289 | my $scope = $p->{'scope'}; 290 | return(error($req, "'scope' is not specified")) unless defined($scope); 291 | return(error($req, "invalid scope")) unless ($scope =~ /^[1-5a-z.]{1,13}$/); 292 | 293 | return iterate_and_push 294 | ($cb, 295 | ['row', 296 | 'SELECT META().id,block_num,primary_key,rowval ' . 297 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 298 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\' ' . 299 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\''], 300 | ['rowupd', 301 | 'SELECT META().id,block_num,primary_key,added,rowval ' . 302 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 303 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\' ' . 304 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\' ' . 305 | 'ORDER BY TONUM(block_num_x)']); 306 | }); 307 | 308 | 309 | $builder->mount 310 | ($CFG::apiprefix . 'table_row_by_pk' => sub { 311 | my $env = shift; 312 | my $req = Plack::Request->new($env); 313 | my $p = $req->parameters(); 314 | my $network = $p->{'network'}; 315 | return(error($req, "'network' is not specified")) unless defined($network); 316 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 317 | 318 | my $code = $p->{'code'}; 319 | return(error($req, "'code' is not specified")) unless defined($code); 320 | return(error($req, "invalid code")) unless ($code =~ /^[1-5a-z.]{1,13}$/); 321 | 322 | my $table = $p->{'table'}; 323 | return(error($req, "'table' is not specified")) unless defined($table); 324 | return(error($req, "invalid table")) unless ($table =~ /^[1-5a-z.]{1,13}$/); 325 | 326 | my $scope = $p->{'scope'}; 327 | return(error($req, "'scope' is not specified")) unless defined($scope); 328 | return(error($req, "invalid scope")) unless ($scope =~ /^[1-5a-z.]{1,13}$/); 329 | 330 | my $pk = $p->{'pk'}; 331 | return(error($req, "'pk' is not specified")) unless defined($pk); 332 | return(error($req, "invalid pk")) unless ($pk =~ /^\d+$/); 333 | 334 | my $ret = undef; 335 | my $rv = $cb->query_slurp 336 | ('SELECT block_num,primary_key,rowval ' . 337 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 338 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\' ' . 339 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\' ' . 340 | ' AND primary_key=\'' . $pk . '\''); 341 | 342 | # there's only one or zero rows 343 | foreach my $row (@{$rv->rows}) 344 | { 345 | $ret = $row; 346 | } 347 | 348 | # process updates 349 | $rv = $cb->query_slurp 350 | ('SELECT added,block_num,primary_key,rowval ' . 351 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 352 | ' AND network=\'' . $network . '\' AND code=\'' . $code . '\' ' . 353 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\' ' . 354 | ' AND primary_key=\'' . $pk . '\' ORDER BY TONUM(block_num_x)'); 355 | 356 | foreach my $row (@{$rv->rows}) 357 | { 358 | if( $row->{'added'} eq 'true' ) 359 | { 360 | delete $row->{'added'}; 361 | $ret = $row; 362 | } 363 | else 364 | { 365 | $ret = undef; 366 | } 367 | } 368 | 369 | return push_one_or_nothing($ret); 370 | }); 371 | 372 | 373 | 374 | 375 | $builder->mount 376 | ($CFG::apiprefix . 'table_rows_by_scope' => sub { 377 | my $env = shift; 378 | my $req = Plack::Request->new($env); 379 | my $p = $req->parameters(); 380 | my $network = $p->{'network'}; 381 | return(error($req, "'network' is not specified")) unless defined($network); 382 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 383 | 384 | my $ctype = $p->{'contract_type'}; 385 | return(error($req, "'contract_type' is not specified")) unless defined($ctype); 386 | return(error($req, "invalid contract_type")) unless ($ctype =~ /^[a-z0-9:_]+$/); 387 | 388 | my $table = $p->{'table'}; 389 | return(error($req, "'table' is not specified")) unless defined($table); 390 | return(error($req, "invalid table")) unless ($table =~ /^[1-5a-z.]{1,13}$/); 391 | 392 | my $scope = $p->{'scope'}; 393 | return(error($req, "'scope' is not specified")) unless defined($scope); 394 | return(error($req, "invalid scope")) unless ($scope =~ /^[1-5a-z.]{1,13}$/); 395 | 396 | return iterate_and_push 397 | ($cb, 398 | ['row', 399 | 'SELECT META().id,block_num,code,primary_key,rowval ' . 400 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 401 | ' AND network=\'' . $network . '\' AND contract_type=\'' . $ctype . '\' ' . 402 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\''], 403 | ['rowupd', 404 | 'SELECT META().id,block_num,added,code,primary_key,rowval ' . 405 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 406 | ' AND network=\'' . $network . '\' AND contract_type=\'' . $ctype . '\' ' . 407 | ' AND tblname=\'' . $table . '\' AND scope=\'' . $scope . '\'' . 408 | 'ORDER BY TONUM(block_num_x)']); 409 | }); 410 | 411 | 412 | 413 | $builder->mount 414 | ($CFG::apiprefix . 'account_history' => sub { 415 | my $env = shift; 416 | my $req = Plack::Request->new($env); 417 | my $p = $req->parameters(); 418 | my $network = $p->{'network'}; 419 | return(error($req, "'network' is not specified")) unless defined($network); 420 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 421 | 422 | my $account = $p->{'account'}; 423 | return(error($req, "'account' is not specified")) unless defined($account); 424 | return(error($req, "invalid account")) unless ($account =~ /^[1-5a-z.]{1,13}$/); 425 | 426 | my $maxrows = $p->{'maxrows'}; 427 | $maxrows = 100 unless defined($maxrows); 428 | 429 | my $filter = ''; 430 | my $block_order = ''; 431 | my $start_block = $p->{'start_block'}; 432 | if( defined($start_block) ) 433 | { 434 | return(error($req, "invalid start_block")) unless ($start_block =~ /^\d+$/); 435 | $filter .= ' AND TONUM(block_num) >= ' . $start_block; 436 | } 437 | 438 | my $end_block = $p->{'end_block'}; 439 | if( defined($end_block) ) 440 | { 441 | return(error($req, "invalid end_block")) unless ($end_block =~ /^\d+$/); 442 | $filter .= ' AND TONUM(block_num) <= ' . $end_block; 443 | } 444 | 445 | if( $filter ne '' ) 446 | { 447 | $block_order = 'TONUM(block_num) DESC,'; 448 | } 449 | 450 | return iterate_and_push 451 | ($cb, 452 | ['tx', 453 | 'SELECT META().id,block_num,block_timestamp,trace, \'false\' as irreversible ' . 454 | 'FROM ' . $CFG::bucket . ' WHERE type=\'transaction_upd\' ' . 455 | ' AND ANY acc IN tx_accounts SATISFIES acc=\'' . $account . '\' END ' . 456 | $filter . 457 | ' ORDER BY ' . $block_order . 'TONUM(trace.action_traces[0].receipt.global_sequence) DESC ' . 458 | ' LIMIT ' . $maxrows], 459 | ['tx', 460 | 'SELECT META().id,block_num,block_timestamp,trace, \'true\' as irreversible ' . 461 | 'FROM ' . $CFG::bucket . ' WHERE type=\'transaction\' ' . 462 | ' AND ANY acc IN tx_accounts SATISFIES acc=\'' . $account . '\' END ' . 463 | $filter . 464 | ' ORDER BY ' . $block_order . 'TONUM(trace.action_traces[0].receipt.global_sequence) DESC ' . 465 | ' LIMIT ' . $maxrows]); 466 | }); 467 | 468 | 469 | 470 | $builder->mount 471 | ($CFG::apiprefix . 'tokens' => sub { 472 | my $env = shift; 473 | my $req = Plack::Request->new($env); 474 | my $p = $req->parameters(); 475 | my $network = $p->{'network'}; 476 | return(error($req, "'network' is not specified")) unless defined($network); 477 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 478 | 479 | my $account = $p->{'account'}; 480 | return(error($req, "'account' is not specified")) unless defined($account); 481 | return(error($req, "invalid account")) unless ($account =~ /^[1-5a-z.]{1,13}$/); 482 | 483 | return iterate_and_push 484 | ($cb, 485 | ['row', 486 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 487 | 'FROM ' . $CFG::bucket . ' use index(tbl_row_04) ' . 488 | 'WHERE type=\'table_row\' ' . 489 | ' AND network=\'' . $network . '\' AND contract_type=\'token:dgoods\' ' . 490 | ' AND tblname=\'dgood\' AND scope=code AND rowval.owner=\'' . $account . '\''], 491 | 492 | ['row', 493 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 494 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 495 | ' AND network=\'' . $network . '\' AND contract_type=\'token:dgoods\' ' . 496 | ' AND tblname=\'accounts\' AND scope=\'' . $account . '\''], 497 | 498 | ['row', 499 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 500 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 501 | ' AND network=\'' . $network . '\' AND contract_type=\'token:simpleassets\' ' . 502 | ' AND tblname=\'sassets\' AND scope=\'' . $account . '\''], 503 | 504 | ['row', 505 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 506 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_row\' ' . 507 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 508 | ' AND tblname=\'assets\' AND scope=\'' . $account . '\''], 509 | 510 | ['rowupd', 511 | 'SELECT META().id,added,contract_type,code,tblname,scope,primary_key,rowval ' . 512 | 'FROM ' . $CFG::bucket . ' use index(tbl_upd_04) ' . 513 | 'WHERE type=\'table_upd\' ' . 514 | ' AND network=\'' . $network . '\' AND contract_type=\'token:dgoods\' ' . 515 | ' AND tblname=\'dgood\' AND scope=code AND rowval.owner=\'' . $account . '\''], 516 | 517 | ['rowupd', 518 | 'SELECT META().id,added,contract_type,code,tblname,scope,primary_key,rowval ' . 519 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 520 | ' AND network=\'' . $network . '\' AND contract_type=\'token:dgoods\' ' . 521 | ' AND tblname=\'accounts\' AND scope=\'' . $account . '\''], 522 | 523 | ['rowupd', 524 | 'SELECT META().id,added,contract_type,code,tblname,scope,primary_key,rowval ' . 525 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 526 | ' AND network=\'' . $network . '\' AND contract_type=\'token:simpleassets\' ' . 527 | ' AND tblname=\'sassets\' AND scope=\'' . $account . '\''], 528 | 529 | ['rowupd', 530 | 'SELECT META().id,added,contract_type,code,tblname,scope,primary_key,rowval ' . 531 | 'FROM ' . $CFG::bucket . ' WHERE type=\'table_upd\' ' . 532 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 533 | ' AND tblname=\'assets\' AND scope=\'' . $account . '\''], 534 | ); 535 | }); 536 | 537 | 538 | $builder->mount 539 | ($CFG::apiprefix . 'aa_tokens_metadata' => sub { 540 | my $env = shift; 541 | my $req = Plack::Request->new($env); 542 | my $p = $req->parameters(); 543 | my $network = $p->{'network'}; 544 | return(error($req, "'network' is not specified")) unless defined($network); 545 | return(error($req, "invalid network")) unless ($network =~ /^\w+$/); 546 | 547 | my $account = $p->{'account'}; 548 | return(error($req, "'account' is not specified")) unless defined($account); 549 | return(error($req, "invalid account")) unless ($account =~ /^[1-5a-z.]{1,13}$/); 550 | 551 | my $rv = $cb->query_iterator 552 | ('SELECT DISTINCT code, rowval.collection_name, rowval.schema_name, rowval.template_id ' . 553 | 'FROM ' . $CFG::bucket . ' use index(tbl_row_03) WHERE type=\'table_row\' ' . 554 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 555 | ' AND tblname=\'assets\' AND scope=\'' . $account . '\' ' . 556 | ' AND rowval.schema_name != \'\' ' . 557 | 'UNION ' . 558 | 'SELECT DISTINCT code, rowval.collection_name, rowval.schema_name, rowval.template_id ' . 559 | 'FROM ' . $CFG::bucket . ' use index(tbl_upd_03) WHERE type=\'table_upd\' ' . 560 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 561 | ' AND tblname=\'assets\' AND scope=\'' . $account . '\' ' . 562 | ' AND rowval.schema_name != \'\''); 563 | 564 | my %schemas_seen; 565 | my %templates_seen; 566 | my @queries; 567 | 568 | while( (my $row = $rv->next()) ) 569 | { 570 | my $code = $row->{'code'}; 571 | my $collection = $row->{'collection_name'}; 572 | my $schema = $row->{'schema_name'}; 573 | my $template = $row->{'template_id'}; 574 | 575 | if( not $schemas_seen{$code}{$collection}{$schema} ) 576 | { 577 | push(@queries, 578 | [ 579 | 'row', 580 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 581 | 'FROM ' . $CFG::bucket . ' use index(tbl_row_05) ' . 582 | 'WHERE type=\'table_row\' ' . 583 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 584 | ' AND tblname=\'schemas\' AND code=\'' . $code . '\' AND scope=\'' . $collection . '\'' . 585 | ' AND rowval.schema_name=\'' . $schema . '\''], 586 | [ 587 | 'rowupd', 588 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 589 | 'FROM ' . $CFG::bucket . ' use index(tbl_upd_05) ' . 590 | 'WHERE type=\'table_upd\' ' . 591 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 592 | ' AND tblname=\'schemas\' AND code=\'' . $code . '\' AND scope=\'' . $collection . '\'' . 593 | ' AND rowval.schema_name=\'' . $schema . '\''], 594 | ); 595 | $schemas_seen{$code}{$collection}{$schema} = 1; 596 | } 597 | 598 | if( not $templates_seen{$code}{$collection}{$template} ) 599 | { 600 | push(@queries, 601 | [ 602 | 'row', 603 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 604 | 'FROM ' . $CFG::bucket . ' use index(tbl_row_06) ' . 605 | 'WHERE type=\'table_row\' ' . 606 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 607 | ' AND tblname=\'templates\' AND code=\'' . $code . '\' AND scope=\'' . $collection . '\'' . 608 | ' AND rowval.template_id=' . $template], 609 | [ 610 | 'rowupd', 611 | 'SELECT META().id,contract_type,code,tblname,scope,primary_key,rowval ' . 612 | 'FROM ' . $CFG::bucket . ' use index(tbl_upd_06) ' . 613 | 'WHERE type=\'table_upd\' ' . 614 | ' AND network=\'' . $network . '\' AND contract_type=\'token:atomicassets\' ' . 615 | ' AND tblname=\'templates\' AND code=\'' . $code . '\' AND scope=\'' . $collection . '\'' . 616 | ' AND rowval.template_id=' . $template], 617 | ); 618 | $templates_seen{$code}{$collection}{$template} = 1; 619 | } 620 | } 621 | 622 | return iterate_and_push($cb, @queries); 623 | }); 624 | 625 | 626 | $builder->to_app; 627 | 628 | 629 | 630 | # Local Variables: 631 | # mode: cperl 632 | # indent-tabs-mode: nil 633 | # cperl-indent-level: 4 634 | # cperl-continued-statement-offset: 4 635 | # cperl-continued-brace-offset: -4 636 | # cperl-brace-offset: 0 637 | # cperl-label-offset: -2 638 | # End: 639 | --------------------------------------------------------------------------------