Managing Contracts
Contract Management: PAYG Auto-Topper Script
#!/bin/bash
#
# CONFIGURATION:
# These are the configuration values used for each service to maintain the 100% uptime capabilities of the script.
#
# SERVICE_NAME: Human-readable name of the blockchain or API service (e.g., "btc-mainnet-fullnode").
# SERVICE_NUMBER: Value for the service name, pulled from the enum here: https://raw.githubusercontent.com/arkeonetwork/arkeo/refs/heads/master/common/service.go
# SERVICE_ARKEO_API: API endpoint for the local Arkeo node (used for creating/querying contracts, e.g., http://127.0.0.1:26657).
# SERVICE_ARKEO_FEE: Default fee to use when creating or updating contracts via Arkeo transactions (e.g., "200uarkeo").
# PROVIDER_PUBKEY: Bech32 public key of the provider (the service operator who receives contract income).
# PROVIDER_SENTINEL_API: API endpoint for the provider's Sentinel proxy (where client requests are sent for metering/billing).
# CLIENT_KEY: Local key name in your keyring for the client wallet (the party opening/contracts).
# CLIENT_KEYRING: Keyring backend for the client key ("test", "file", "os", etc.).
# CONTRACT_TYPE: Type of contract: 0 = subscription (fixed time), 1 = pay-as-you-go (usage-based).
# CONTRACT_AUTH: Authorization model: 0 = STRICT (per-request client signatures), 1 = OPEN (no signatures required).
# CONTRACT_DEPOSIT: Total amount of tokens (in smallest denom) to lock in the contract (e.g., "100000000" for 1 ARKEO).
# CONTRACT_DURATION: Duration of contract in blocks (for subscriptions), or can be short for PAYG.
# CONTRACT_RATE: Cost per request (matches provider's advertised rate, in smallest denom).
# CONTRACT_QPM: Maximum queries per minute allowed under the contract (for rate limiting).
# CONTRACT_SETTLEMENT: Number of blocks after contract expiry in which the provider can still submit claims.
# CONTRACT_DELEGATE: (Optional) Bech32 pubkey of a delegate permitted to spend or claim from this contract.
#
CHAIN_ID="arkeo-main-v1"
CLIENT_KEY="Arkeo-Main-Validator-3"
CLIENT_KEYRING="test"
ARKEO_SERVICE_API="127.0.0.1:26657"
ARKEO_SERVICE_FEE="200uarkeo"
CLIENT_RAW_PUBKEY=$(arkeod keys show "$CLIENT_KEY" --output json --keyring-backend="$CLIENT_KEYRING" | jq -r .pubkey | jq -r .key)
CLIENT_PUBKEY=$(arkeod debug pubkey-raw "$CLIENT_RAW_PUBKEY" -t secp256k1 | grep 'Bech32 Acc:' | sed "s|Bech32 Acc: ||g")
CONTRACT_OVERLAP_THRESHOLD=20
SERVICES_LIST=(
'{
"SERVICE_NAME": "btc-mainnet-fullnode",
"SERVICE_NUMBER": 10,
"PROVIDER_PUBKEY": "arkeopub1addwnpepqfn52r6xng2wwfrgz2tm5yvscq42k3yu3ky9cg3kw5s6p0qg7tfx75uwq3z",
"PROVIDER_SENTINEL_API": "http://127.0.0.1:3636",
"CONTRACT_TYPE": 1,
"CONTRACT_AUTH": 0,
"CONTRACT_DEPOSIT": 100000000,
"CONTRACT_DURATION": 100,
"CONTRACT_RATE": "10000uarkeo",
"CONTRACT_QPM": 20,
"CONTRACT_SETTLEMENT": 100,
"CONTRACT_DELEGATE": ""
}'
)
#
# OPEN ARKEO CONTRACT:
# This function is called when there are no contracts found and to create a second overlapping contract when the active contract reached 25%.
#
open_arkeo_contract() {
# Open the contract and capture the txhash
OPEN_CONTRACT_RESULT=$(arkeod tx arkeo open-contract \
"$PROVIDER_PUBKEY" \
"$SERVICE_NAME" \
"$CLIENT_PUBKEY" \
"$CONTRACT_TYPE" \
"$CONTRACT_DEPOSIT" \
"$CONTRACT_DURATION" \
"$CONTRACT_RATE" \
"$CONTRACT_QPM" \
"$CONTRACT_SETTLEMENT" \
"$CONTRACT_AUTH" \
"$CONTRACT_DELEGATE" \
--from="$CLIENT_KEY" \
--keyring-backend="$CLIENT_KEYRING" \
--fees="$ARKEO_SERVICE_FEE" \
--node tcp://$ARKEO_SERVICE_API \
-y --output json)
TXHASH=$(echo "$OPEN_CONTRACT_RESULT" | jq -r '.txhash // .TxHash // empty')
if [ -z "$TXHASH" ] || [ "$TXHASH" == "null" ]; then
echo "Failed to get txhash! Raw result:"
echo "$OPEN_CONTRACT_RESULT"
exit 1
fi
echo "TxHash: $TXHASH"
# Wait for transaction to be committed, retry if not present yet
TX_RESULT=""
for i in {1..10}; do
TX_RESULT=$(arkeod query tx "$TXHASH" --output json 2>/dev/null)
if echo "$TX_RESULT" | jq . > /dev/null 2>&1; then
break
fi
sleep 2
done
CONTRACT_ID=$(echo "$TX_RESULT" | jq -r '
.events[]?
| select(.type == "arkeo.arkeo.EventOpenContract")
| .attributes[]?
| select(.key == "contract_id")
| .value
' | tr -d '"')
if [ -z "$CONTRACT_ID" ]; then
echo "Could not extract contract id. Raw tx result:"
echo "$TX_RESULT" | jq
exit 1
fi
echo "Contract ID: $CONTRACT_ID"
# Sign POST request (update config)
NONCE_DATE=$(date +%s)
MSG_POST="$CONTRACT_ID:$NONCE_DATE:"
SIG_POST=$(signhere -u "$CLIENT_KEY" -m "$MSG_POST" | tail -n 1)
ARKAUTH_POST="$CONTRACT_ID:$NONCE_DATE:$SIG_POST"
API_URL_POST="$PROVIDER_SENTINEL_API/manage/contract/$CONTRACT_ID?arkcontract=$ARKAUTH_POST"
echo "POST URL: $API_URL_POST"
# Prepare the whitelist update JSON
read -r -d '' WHITELIST_JSON <<EOF
{
"white_listed_ip_addresses": [],
"per_user_rate_limit": 1000,
"cors": {
"allow_origins": ["*"],
"allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["*"]
}
}
EOF
echo "Whitelist: $WHITELIST_JSON"
echo "API Url: $API_URL_POST"
# Config contract data
curl -X POST -H "Content-Type: application/json" -d "$WHITELIST_JSON" "$API_URL_POST"
}
#
# SERVICE LOOP:
# Main loop to iterate over each service and manage contracts accordingly
#
echo
echo "Arkeo Pay-As-You-Go Contract Topper"
echo " $(date)"
for SERVICE in "${SERVICES_LIST[@]}"; do
#
# SERVICE VARIABLE MAPPING
# Map the configuration values to variables.
#
SERVICE_NAME=$(echo "$SERVICE" | jq -r '.service_name // .SERVICE_NAME // empty')
SERVICE_NUM=$(echo "$SERVICE" | jq -r '.service_number // .SERVICE_NUMBER // empty')
SERVICE_ARKEO_API=$(echo "$SERVICE" | jq -r '.service_arkeo_api // .SERVICE_ARKEO_API // empty')
SERVICE_ARKEO_FEE=$(echo "$SERVICE" | jq -r '.service_arkeo_fee // .SERVICE_ARKEO_FEE // empty')
PROVIDER_PUBKEY=$(echo "$SERVICE" | jq -r '.provider_pubkey // .PROVIDER_PUBKEY // empty')
PROVIDER_SENTINEL_API=$(echo "$SERVICE" | jq -r '.provider_sentinel_api // .PROVIDER_SENTINEL_API // empty')
CONTRACT_TYPE=$(echo "$SERVICE" | jq -r '.contract_type // .CONTRACT_TYPE // empty')
CONTRACT_AUTH=$(echo "$SERVICE" | jq -r '.contract_auth // .CONTRACT_AUTH // empty')
CONTRACT_DEPOSIT=$(echo "$SERVICE" | jq -r '.contract_deposit // .CONTRACT_DEPOSIT // empty')
CONTRACT_DURATION=$(echo "$SERVICE" | jq -r '.contract_duration // .CONTRACT_DURATION // empty')
CONTRACT_RATE=$(echo "$SERVICE" | jq -r '.contract_rate // .CONTRACT_RATE // empty')
CONTRACT_QPM=$(echo "$SERVICE" | jq -r '.contract_qpm // .CONTRACT_QPM // empty')
CONTRACT_SETTLEMENT=$(echo "$SERVICE" | jq -r '.contract_settlement // .CONTRACT_SETTLEMENT // empty')
CONTRACT_DELEGATE=$(echo "$SERVICE" | jq -r '.contract_delegate // .CONTRACT_DELEGATE // empty')
if [[ -z "$SERVICE_NAME" || -z "$SERVICE_NUM" || -z "$PROVIDER_PUBKEY" || -z "$CLIENT_PUBKEY" ]]; then
echo
echo "Service: $SERVICE_NAME:"
echo " Service entry is missing required fields."
continue
fi
#
# LIST CONTRACTS
# This pulls a long list of contracts and filters them for the client and provider service.
#
CONTRACTS_JSON="$(arkeod query arkeo list-contracts --output json)"
CONTRACTS=$(echo "$CONTRACTS_JSON" | jq -c \
--arg sn "$SERVICE_NUM" \
--arg prov "$PROVIDER_PUBKEY" \
--arg cli "$CLIENT_PUBKEY" '
[.contract[]
| select(.type == "PAY_AS_YOU_GO")
| select(.service == ($sn | tonumber))
| select(.provider == $prov)
| select(.client == $cli)
| select(.settlement_height == "0")
| select(.deposit != "0")
] | sort_by(.id | tonumber)[]
')
#
# NO CONTRACTS IN SERVICE
# Let's create a contract for the service since it didn't find an active one.
#
if [[ -z "$CONTRACTS" ]]; then
echo
echo "Service: $SERVICE_NAME:"
echo " No active PAY_AS_YOU_GO contracts found."
echo " Attempting to open a new active contract."
open_arkeo_contract
continue
fi
#
# CONTRACTS LOOP:
# Process and display information for each active contract
#
PRINTED_SERVICE_HEADER=0
echo "$CONTRACTS" | while read -r CONTRACT; do
#
# SERVICE HEADER:
# Print this header once.
#
if [[ "$PRINTED_SERVICE_HEADER" -eq 0 ]]; then
echo
echo "Service: $SERVICE_NAME"
PRINTED_SERVICE_HEADER=1
fi
#
# CONTRACT CALCULATIONS:
# Various calculations to determine percentages of usage and duration.
#
#echo "$CONTRACT" | jq
CONTRACT_ID=$(echo "$CONTRACT" | jq -r '.id')
#echo "Contract ID: $CONTRACT_ID"
DEPOSIT_VAL=$(echo "$CONTRACT" | jq -r '.deposit | tonumber')
RATE_AMOUNT=$(echo "$CONTRACT" | jq -r '.rate.amount | tonumber')
CLAIMS_API="$PROVIDER_SENTINEL_API/claims?contract_id=$CONTRACT_ID&client=$CLIENT_PUBKEY"
NONCE=$(curl -s "$CLAIMS_API" | jq .highestNonce)
if [ -z "$NONCE" ] || [ "$NONCE" == "null" ]; then
NONCE=0
fi
if [[ -z "$CONTRACT_ID" || -z "$DEPOSIT_VAL" || -z "$NONCE" || -z "$RATE_AMOUNT" ]]; then
continue
fi
#echo "Nonce: $NONCE"
#echo "Rate Amount: $RATE_AMOUNT"
USED_AMOUNT=$(($NONCE * $RATE_AMOUNT))
REMAINDER=$(($DEPOSIT_VAL - $USED_AMOUNT))
#echo "Deposit Val: $DEPOSIT_VAL"
#echo "Used Amount: $USED_AMOUNT"
#echo "Remainder: $REMAINDER"
if [[ "$DEPOSIT_VAL" -eq 0 ]]; then
PERCENT_LEFT=0
PERCENT_USED=0
else
PERCENT_LEFT=$(awk "BEGIN {printf \"%.2f\", ($REMAINDER/$DEPOSIT_VAL)*100}")
PERCENT_USED=$(awk "BEGIN {printf \"%.2f\", 100 - ($REMAINDER/$DEPOSIT_VAL)*100}")
fi
CURRENT_HEIGHT=$(arkeod status | jq -r .sync_info.latest_block_height)
CONTRACT_START=$(echo "$CONTRACT" | jq -r '.height | tonumber')
CONTRACT_DURATION=$(echo "$CONTRACT" | jq -r '.duration | tonumber')
CONTRACT_EXPIRE_HEIGHT=$((CONTRACT_START + CONTRACT_DURATION))
BLOCKS_LEFT=$((CONTRACT_EXPIRE_HEIGHT - CURRENT_HEIGHT))
if (( CONTRACT_DURATION > 0 )); then
PERCENT_TIL_EXPIRE=$(awk "BEGIN {printf \"%.2f\", ($BLOCKS_LEFT/$CONTRACT_DURATION)*100}")
(( $(awk "BEGIN {print ($PERCENT_TIL_EXPIRE < 0)}") )) && PERCENT_TIL_EXPIRE=0
else
PERCENT_TIL_EXPIRE=0
fi
DURATION_USED=$(awk "BEGIN {printf \"%.2f\", 100 - $PERCENT_TIL_EXPIRE}")
if (( BLOCKS_LEFT <= 0 )); then
#
# CONTRACT SETTLEMENT PHASE
# The contract has expired, but is still in a settlement phase. The overlap contract should be used for calls.
#
echo " Contract #$CONTRACT_ID, Deposit: ${DEPOSIT_VAL}uarkeo, Expires at Block: $CONTRACT_EXPIRE_HEIGHT"
echo " $PERCENT_LEFT% Deposit Remaining"
# Calculate settlement percentage left
CONTRACT_START_HEIGHT=$(echo "$CONTRACT" | jq -r '.height | tonumber')
CONTRACT_DURATION=$(echo "$CONTRACT" | jq -r '.duration | tonumber')
SETTLEMENT_DURATION=$(echo "$CONTRACT" | jq -r '.settlement_duration | tonumber')
SETTLEMENT_BLOCKS_LEFT=$(( (CONTRACT_START_HEIGHT + CONTRACT_DURATION + SETTLEMENT_DURATION) - CURRENT_HEIGHT ))
PERCENT_TIL_SETTLEMENT=$(awk "BEGIN {printf \"%.2f\", ($SETTLEMENT_BLOCKS_LEFT/$SETTLEMENT_DURATION)*100}")
echo " $PERCENT_TIL_SETTLEMENT% Settlement Remaining"
else
#
# CONTRACT ACTIVE PHASE
# Active contract used for calls.
#
echo " Contract #$CONTRACT_ID: Deposit: ${DEPOSIT_VAL}uarkeo, Expires at Block: $CONTRACT_EXPIRE_HEIGHT"
echo " $PERCENT_LEFT% Deposit Remaining"
echo " $PERCENT_TIL_EXPIRE% Duration Remaining"
fi
#
# CONTRACT OVERLAP CREATION
# When the active contract reaches a duration threshold, this triggers the creation of an overlapping contract to take over when the active one expires.
#
PERCENT_TIL_EXPIRE_INT=${PERCENT_TIL_EXPIRE%.*}
CONTRACT_COUNT=$(echo "$CONTRACTS" | wc -l)
if (( PERCENT_TIL_EXPIRE_INT <= CONTRACT_OVERLAP_THRESHOLD )) && (( CONTRACT_COUNT == 1 )); then
echo
echo " Contract ():"
echo " Active Contract is getting low."
echo " Attempting to open a new contract to use next..."
open_arkeo_contract
fi
done
done
echoContract Management: PAYG Auto-Fetch Test Script
Last updated