diff --git a/tools/peer-heights b/tools/peer-heights new file mode 100755 index 00000000..4ebc21db --- /dev/null +++ b/tools/peer-heights @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Requires: 'qort' script in PATH, and 'jq' utility installed + +set -e + +# Any extra args passed to us are also passed to 'qort', just prior to '-p peers' +qort $@ -p peers | \ + jq -r 'def lpad($len): + tostring | ($len - length) as $l | (" " * $l)[:$l] + .; + .[] | + select(has("lastHeight")) | + "\(.address | lpad(22)) (\(.version)), height \(.lastHeight), sig: \(.lastBlockSignature[0:8]), ts \(.lastBlockTimestamp / 1e3 | strftime("%Y-%m-%d %H:%M:%S"))"' diff --git a/tools/qort b/tools/qort new file mode 100755 index 00000000..732fb825 --- /dev/null +++ b/tools/qort @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +# default output post-processor +postproc=cat + +# Qortal defaults +port=12391 +example_host=node10.qortal.org + +# called-as name +name="${0##*/}" + +while [ -n "$*" ]; do + case $1 in + -p) + shift + postproc="json_pp -f json -t json -json_opt utf8,pretty" + ;; + + [Dd][Ee][Ll][Ee][Tt][Ee]) + shift + method="-X DELETE" + ;; + + -l) + shift + src="--interface localhost" + ;; + + -t) + shift + testnet=true + ;; + + -j) + shift + content_type="Content-Type: application/json" + ;; + + *) + break + ;; + esac +done + +if [ "${name}" = "qort" ]; then + port=${testnet:+62391} + port=${port:-12391} + example_host=node10.qortal.org +fi + +if [ -z "$*" ]; then + echo "usage: $name [-l] [-p] [-t] [DELETE] []" + echo "-l: use localhost as source address" + echo "-p: pretty-print JSON output" + echo "-t: use testnet port" + echo "example (using localhost:${port}): $name -p blocks/last" + echo "example: $name -p http://${example_host}:${port}/blocks/last" + echo "example: BASE_URL=http://${example_host}:${port} $name -p blocks/last" + echo "example: BASE_URL=${example_host} $name -p blocks/last" + echo "example: $name -l DELETE peers/known" + exit +fi + +url=$1 +shift + +if [ "${url:0:4}" != "http" ]; then + base_url=${BASE_URL-localhost:${port}} + + if [ "${base_url:0:4}" != "http" ]; then + base_url="http://${base_url}" + fi + + if [ -n "${base_url/#*:[0-9[0-9]*}" ]; then + base_url="${base_url%%/}:${port}" + fi + + url="${base_url%%/}/${url#/}" +fi + +if [ "$#" != 0 ]; then + data="--data" +fi + +curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc} +echo diff --git a/tools/tx.pl b/tools/tx.pl new file mode 100755 index 00000000..db6958e2 --- /dev/null +++ b/tools/tx.pl @@ -0,0 +1,353 @@ +#!/usr/bin/env perl + +use JSON; +use warnings; +use strict; + +use Getopt::Std; +use File::Basename; + +our %opt; +getopts('dpst', \%opt); + +my $proc = basename($0); + +if (@ARGV < 1) { + print STDERR "usage: $proc [-d] [-p] [-s] [-t] []\n"; + print STDERR "-d: debug, -p: process (broadcast) transaction, -s: sign, -t: testnet\n"; + print STDERR "example: $proc PAYMENT P22kW91AJfDNBj32nVii292hhfo5AgvUYPz5W12ExsjE QxxQZiK7LZBjmpGjRz1FAZSx9MJDCoaHqz 0.1\n"; + print STDERR "example: $proc JOIN_GROUP X92h3hf9k20kBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n"; + print STDERR "example: BASE_URL=node10.qortal.org $proc JOIN_GROUP CB2DW91AJfd47432nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n"; + print STDERR "example: $proc -p sign C4ifh827ffDNBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 111jivxUwerRw...Fjtu\n"; + print STDERR "for help: $proc all\n"; + print STDERR "for help: $proc REGISTER_NAME\n"; + exit 2; +} + +our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391'; +our $DEFAULT_FEE = 0.001; + +our %TRANSACTION_TYPES = ( + payment => { + url => 'payments/pay', + required => [qw(recipient amount)], + key_name => 'senderPublicKey', + }, + # groups + set_group => { + url => 'groups/setdefault', + required => [qw(defaultGroupId)], + key_name => 'creatorPublicKey', + }, + create_group => { + url => 'groups/create', + required => [qw(groupName description isOpen approvalThreshold)], + key_name => 'creatorPublicKey', + }, + update_group => { + url => 'groups/update', + required => [qw(groupId newOwner newDescription newIsOpen newApprovalThreshold)], + key_name => 'ownerPublicKey', + }, + join_group => { + url => 'groups/join', + required => [qw(groupId)], + key_name => 'joinerPublicKey', + }, + leave_group => { + url => 'groups/leave', + required => [qw(groupId)], + key_name => 'leaverPublicKey', + }, + group_invite => { + url => 'groups/invite', + required => [qw(groupId invitee)], + key_name => 'adminPublicKey', + }, + group_kick => { + url => 'groups/kick', + required => [qw(groupId member reason)], + key_name => 'adminPublicKey', + }, + add_group_admin => { + url => 'groups/addadmin', + required => [qw(groupId member)], + key_name => 'ownerPublicKey', + }, + group_approval => { + url => 'groups/approval', + required => [qw(pendingSignature approval)], + key_name => 'adminPublicKey', + }, + # assets + issue_asset => { + url => 'assets/issue', + required => [qw(assetName description quantity isDivisible)], + key_name => 'issuerPublicKey', + }, + update_asset => { + url => 'assets/update', + required => [qw(assetId newOwner)], + key_name => 'ownerPublicKey', + }, + transfer_asset => { + url => 'assets/transfer', + required => [qw(recipient amount assetId)], + key_name => 'senderPublicKey', + }, + create_order => { + url => 'assets/order', + required => [qw(haveAssetId wantAssetId amount price)], + key_name => 'creatorPublicKey', + }, + # names + register_name => { + url => 'names/register', + required => [qw(name data)], + key_name => 'registrantPublicKey', + }, + update_name => { + url => 'names/update', + required => [qw(newName newData)], + key_name => 'ownerPublicKey', + }, + # reward-shares + reward_share => { + url => 'addresses/rewardshare', + required => [qw(recipient rewardSharePublicKey sharePercent)], + key_name => 'minterPublicKey', + }, + # arbitrary + arbitrary => { + url => 'arbitrary', + required => [qw(service dataType data)], + key_name => 'senderPublicKey', + }, + # chat + chat => { + url => 'chat', + required => [qw(data)], + optional => [qw(recipient isText isEncrypted)], + key_name => 'senderPublicKey', + defaults => { isText => 'true' }, + pow_url => 'chat/compute', + }, + # misc + publicize => { + url => 'addresses/publicize', + required => [], + key_name => 'senderPublicKey', + pow_url => 'addresses/publicize/compute', + }, + # Cross-chain trading + build_trade => { + url => 'crosschain/build', + required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)], + optional => [qw(tradeTimeout)], + key_name => 'creatorPublicKey', + defaults => { tradeTimeout => 10800 }, + }, + trade_recipient => { + url => 'crosschain/tradeoffer/recipient', + required => [qw(atAddress recipient)], + key_name => 'creatorPublicKey', + remove => [qw(timestamp reference fee)], + }, + trade_secret => { + url => 'crosschain/tradeoffer/secret', + required => [qw(atAddress secret)], + key_name => 'recipientPublicKey', + remove => [qw(timestamp reference fee)], + }, + # These are fake transaction types to provide utility functions: + sign => { + url => 'transactions/sign', + required => [qw{transactionBytes}], + }, +); + +my $tx_type = lc(shift(@ARGV)); + +if ($tx_type eq 'all') { + printf STDERR "Transaction types: %s\n", join(', ', sort { $a cmp $b } keys %TRANSACTION_TYPES); + exit 2; +} + +my $tx_info = $TRANSACTION_TYPES{$tx_type}; + +if (!$tx_info) { + printf STDERR "Transaction type '%s' unknown\n", uc($tx_type); + exit 1; +} + +my @required = @{$tx_info->{required}}; + +if (@ARGV < @required + 1) { + printf STDERR "usage: %s %s %s", $proc, uc($tx_type), join(' ', map { "<$_>"} @required); + printf STDERR " %s", join(' ', map { "[$_ <$_>]" } @{$tx_info->{optional}}) if exists $tx_info->{optional}; + print "\n"; + exit 2; +} + +my $priv_key = shift @ARGV; + +my $account = account($priv_key); +my $raw; + +if ($tx_type ne 'sign') { + my %extras; + + foreach my $required_arg (@required) { + $extras{$required_arg} = shift @ARGV; + } + + # For CHAT we use a random reference + if ($tx_type eq 'chat') { + $extras{reference} = api('utils/random?length=64'); + } + + %extras = (%extras, %{$tx_info->{defaults}}) if exists $tx_info->{defaults}; + + %extras = (%extras, @ARGV); + + $raw = build_raw($tx_type, $account, %extras); + printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p}); + + # Some transaction types require proof-of-work, e.g. CHAT + if (exists $tx_info->{pow_url}) { + $raw = api($tx_info->{pow_url}, $raw); + printf "Raw with PoW: %s\n", $raw if $opt{d}; + } +} else { + $raw = shift @ARGV; + $opt{s}++; +} + +if ($opt{s}) { + my $signed = sign($account->{private}, $raw); + printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign'; + + if ($opt{p}) { + my $processed = process($signed); + printf "Processed: %s\n", $processed if $opt{d}; + } + + my $hex = api('utils/frombase58', $signed); + # sig is last 64 bytes / 128 chars + my $sighex = substr($hex, -128); + + my $sig58 = api('utils/tobase58/{hex}', '', '{hex}', $sighex); + printf "Signature: %s\n", $sig58; +} + +sub account { + my ($creator) = @_; + + my $account = { private => $creator }; + $account->{public} = api('utils/publickey', $creator); + $account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public}); + + return $account; +} + +sub build_raw { + my ($type, $account, %extras) = @_; + + my $tx_info = $TRANSACTION_TYPES{$type}; + die("unknown tx type: $type\n") unless defined $tx_info; + + my $ref = exists $extras{reference} ? $extras{reference} : lastref($account->{address}); + + my %json = ( + timestamp => time * 1000, + reference => $ref, + fee => $DEFAULT_FEE, + ); + + $json{$tx_info->{key_name}} = $account->{public} if exists $tx_info->{key_name}; + + foreach my $required (@{$tx_info->{required}}) { + die("missing tx field: $required\n") unless exists $extras{$required}; + } + + while (my ($key, $value) = each %extras) { + $json{$key} = $value; + } + + if (exists $tx_info->{remove}) { + foreach my $key (@{$tx_info->{remove}}) { + delete $json{$key}; + } + } + + my $json = "{\n"; + while (my ($key, $value) = each %json) { + if (ref($value) eq 'ARRAY') { + $json .= "\t\"$key\": [],\n"; + } else { + $json .= "\t\"$key\": \"$value\",\n"; + } + } + # remove final comma + substr($json, -2, 1) = ''; + $json .= "}\n"; + + printf "%s:\n%s\n", $type, $json if $opt{d}; + + my $raw = api($tx_info->{url}, $json); + return $raw; +} + +sub sign { + my ($private, $raw) = @_; + + my $json = <<" __JSON__"; + { + "privateKey": "$private", + "transactionBytes": "$raw" + } + __JSON__ + + return api('transactions/sign', $json); +} + +sub process { + my ($signed) = @_; + + return api('transactions/process', $signed); +} + +sub lastref { + my ($address) = @_; + + return api('addresses/lastreference/{address}', '', '{address}', $address) +} + +sub api { + my ($endpoint, $postdata, @args) = @_; + + my $url = $endpoint; + my $method = 'GET'; + + for (my $i = 0; $i < @args; $i += 2) { + my $placemarker = $args[$i]; + my $value = $args[$i + 1]; + + $url =~ s/$placemarker/$value/g; + } + + my $curl = "curl --silent --output - --url '$BASE_URL/$url'"; + if (defined $postdata && $postdata ne '') { + $postdata =~ tr|\n| |s; + $curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'"; + $method = 'POST'; + } + my $response = `$curl 2>/dev/null`; + chomp $response; + + if ($response eq '' || substr($response, 0, 6) eq '' || $response =~ m/(^\{|,)"error":(\d+)[,}]/) { + die("API call '$method $BASE_URL/$endpoint' failed:\n$response\n"); + } + + return $response; +}