Author SHA1 Message Date
zramsay fc53cae044 idk claude 2025-06-26 11:43:31 -04:00
zramsay 84fd528619 dont process all previous requests on boot 2025-06-26 11:16:55 -04:00
zramsay fa4628a153 ts errors 2025-06-26 11:03:58 -04:00
zramsay aa27f4ff18 fix 2025-06-26 10:58:21 -04:00
zramsay 15187227e9 try 2025-06-26 10:45:32 -04:00
zramsay 429bb62cf1 uatom. string 2025-05-02 14:14:09 -04:00
zramsay a8be189229 use local laconic-so for testing 2025-05-01 07:51:35 -04:00
zramsay 50b1877e8c accept atom payments 2025-04-30 11:08:00 -04:00
prathamesh 6322ebd30f Pass k8s cluster IP when deploying webapps (#25)
Part of https://www.notion.so/Support-custom-domains-in-deploy-laconic-com-18aa6b22d4728067a44ae27090c02ce5 and cerc-io/snowballtools-base#47
Requires cerc-io/stack-orchestrator#963

Reviewed-on: cerc-io/webapp-deployment-status-api#25
Reviewed-by: ashwin <ashwin@noreply.git.vdb.to>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-02-04 13:27:17 +00:00
prathamesh 7f83bc6c89 Fix recreate-on-deploy flag in deploy command (#23)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Reviewed-on: cerc-io/webapp-deployment-status-api#23
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2024-10-29 07:08:11 +00:00
prathameshandAdw8 48af6dea2c Remove payment requirement for undeployments (#22)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/webapp-deployment-status-api#22
Co-authored-by: Prathamesh Musale <prathamesh@noreply.git.vdb.to>
Co-committed-by: Prathamesh Musale <prathamesh@noreply.git.vdb.to>
2024-10-29 06:27:01 +00:00
prathamesh cad9406579 Handle deployment auctions in a separate process (#21)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75) and cerc-io/stack-orchestrator#948
Requires cerc-io/stack-orchestrator#957

Reviewed-on: cerc-io/webapp-deployment-status-api#21
Reviewed-by: ashwin <ashwin@noreply.git.vdb.to>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2024-10-25 09:00:46 +00:00
13 changed files with 941 additions and 257 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ CLEAN_CONTAINERS=false
SYSTEM_PRUNE=false
WEBAPP_IMAGE_PRUNE=true
CHECK_INTERVAL=5
FQDN_POLICY="allow"
FQDN_POLICY="allow"
DEPLOYMENT_IP="k8s.cluster.ip.address"
+5 -2
View File
@@ -15,6 +15,9 @@ RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/lat
chmod +x ./laconic-so && \
mv ./laconic-so /usr/bin/laconic-so
# for testing, put so in the root of this repo prior to running `build-containers`
#COPY laconic-so /usr/bin/laconic-so
# laconic-registry-cli
RUN npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/ && \
npm install -g @cerc-io/laconic-registry-cli && \
@@ -44,5 +47,5 @@ COPY . /app/
WORKDIR /app/
RUN rm -rf node_modules && yarn && yarn clean && yarn build:release
COPY run.sh .
CMD ["./run.sh"]
COPY scripts .
CMD ["./scripts/run.sh"]
+40 -3
View File
@@ -1,7 +1,7 @@
# webapp-deployment-status-api
This API provides status information about webapp deployment requests and a mechanism for upload encrypted configuration
files used by those requests.
files used by those requests. It supports payments in both Laconic testnet (alnt) and Cosmos mainnet (ATOM).
## Build and Run
@@ -11,6 +11,22 @@ yarn build
yarn start
```
## API Endpoints
### Status Endpoints
- `GET /` - Get status of all deployment requests
- `GET /:id` - Get status of a specific deployment request
- `GET /:id/log` - Get logs for a specific deployment request
- `GET /log/:id` - Get logs for a specific deployment request (deprecated)
### Configuration Upload
- `POST /upload/config` - Upload encrypted configuration file
### ATOM Payment Verification
- `POST /verify/atom-payment` - Verify a Cosmos ATOM payment transaction
- Request body: `{ "txHash": "string", "minAmount": number (optional), "markAsUsed": boolean (optional) }`
- Response: `{ "valid": boolean, "reason": string (if invalid), "amount": number (if valid), "sender": string (if valid), "alreadyUsed": boolean (if transaction was already used) }`
## Configuration
### Keys
@@ -44,6 +60,8 @@ record:
minimumPayment: 100alnt
paymentAddress: laconic1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
publicKey: mQGNBGbJUk0BDAC3j3CiaVtoEf1jrgtsjJnTA5u1a3BExP72mv0eE8y84TgY5rVcf ...
atomPaymentAddress: cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
minimumAtomPayment: 1
```
This record can most easily be created using `laconic-so publish-deployer-to-registry`.
@@ -54,7 +72,9 @@ laconic-so publish-deployer-to-registry \
--api-url https://webapp-deployer-api.my.domain.com
--public-key-file webapp-deployer-api.my.domain.com.pgp.pub \
--lrn lrn://laconic/deployers/webapp-deployer-api.my.domain.com \
--min-required-payment 100
--min-required-payment 100 \
--atom-payment-address cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry \
--min-atom-payment 1
```
This will create the record in the proper format and assign its LRN.
@@ -83,11 +103,13 @@ Users can now request deployment using the LRN of the deployer. This will allow
1. Discover the API URL for config uploads.
1. Obtain the public key for encrypting config.
1. See the minimum required payment.
1. See the minimum required payment (both alnt and ATOM if configured).
The request can be made using `laconic-so request-webapp-deployment`. This will handle encrypting and uploading the
config automatically, as well as making a payment (if necessary).
Payment can be made using either Laconic testnet (alnt) or Cosmos mainnet (ATOM). For Cosmos ATOM payments, you'll need to make the payment separately and provide the transaction hash.
```bash
laconic-so request-webapp-deployment \
--laconic-config ~/.laconic/registry.yml \
@@ -95,6 +117,13 @@ laconic-so request-webapp-deployment \
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
--env-file hello.env \
--make-payment auto
# OR for Cosmos ATOM payment
laconic-so request-webapp-deployment \
--laconic-config ~/.laconic/registry.yml \
--deployer lrn://laconic/deployers/webapp-deployer-api.my.domain.com \
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
--env-file hello.env \
--use-payment <your_cosmos_transaction_hash>
```
Alternatively, users can also use a deployment auction they created instead of making the payment to any specific deployer directly:
@@ -133,6 +162,14 @@ OPENPGP_PASSPHRASE="SECRET"
OPENPGP_PRIVATE_KEY_FILE="/etc/config/webapp-deployer-api.my.domain.com.pgp.key"
LACONIC_CONFIG="/etc/config/registry.yml"
LRN=lrn://laconic/deployers/webapp-deployer-api.my.domain.com
CHECK_INTERVAL=15
# Cosmos ATOM payment configuration
ATOM_PAYMENT_ADDRESS="cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry"
MIN_ATOM_PAYMENT=1
COSMOS_RPC_ENDPOINT="https://cosmos-rpc.example.com"
AUCTION_CHECK_INTERVAL=10
HANDLE_AUCTION_REQUESTS=true
AUCTION_BID_AMOUNT=50000
```
+9
View File
@@ -8,6 +8,15 @@ services:
ports:
- 9555
cerc-webapp-auction-handler:
image: cerc/webapp-deployer-backend:local
restart: always
environment:
RUN_AUCTIONS_HANDLER: "true"
volumes:
- srv:/srv
- config:/etc/config:ro
volumes:
config:
srv:
+2 -1
View File
@@ -36,8 +36,9 @@
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
"license": "Apache-2.0",
"dependencies": {
"@cerc-io/laconic-sdk": "^0.1.15",
"@cerc-io/registry-sdk": "^0.2.11",
"@openpgp/web-stream-tools": "^0.1.3",
"axios": "^1.6.7",
"body-parser": "^1.20.2",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
-204
View File
@@ -1,204 +0,0 @@
#!/bin/bash
function is_privileged {
ip link add dummy0 type dummy >/dev/null
if [[ $? -eq 0 ]]; then
echo "true"
ip link delete dummy0 >/dev/null
else
echo "false"
fi
}
if [ -z "$DEPLOYMENT_DNS_SUFFIX" ]; then
echo "DEPLOYMENT_DNS_SUFFIX is required."
exit 2
fi
if [ -z "$DEPLOYMENT_RECORD_NAMESPACE" ]; then
echo "DEPLOYMENT_RECORD_NAMESPACE is required."
exit 2
fi
if [ -z "$IMAGE_REGISTRY" ]; then
echo "IMAGE_REGISTRY is required."
exit 2
fi
if [ ! -f "/etc/config/laconic.yml" ]; then
echo "/etc/config/laconic.yml is required."
exit 2
fi
if [ ! -f "/etc/config/kube.yml" ]; then
echo "/etc/config/kube.yml is required."
exit 2
fi
AUCTION_OPTS=""
if [ "$HANDLE_AUCTION_REQUESTS" = "true" ]; then
if [ -z "$AUCTION_BID_AMOUNT" ]; then
echo "AUCTION_BID_AMOUNT is required when handling auction requsts."
exit 2
fi
AUCTION_OPTS="--auction-requests"
fi
STORAGE_ROOT="${STORAGE_ROOT:-/srv}"
DEPLOYMENTS_DIR="${DEPLOYMENTS_DIR:-$STORAGE_ROOT/deployments}"
LOG_DIR="${LOG_DIR:-$STORAGE_ROOT/logs}"
CONTAINERS_DIR="${CONTAINER_DIR:-$STORAGE_ROOT/containers}"
if [[ ! -d "${DEPLOYMENTS_DIR}" ]]; then
mkdir -p "${DEPLOYMENTS_DIR}"
fi
LOG_OPTS=""
if [[ -n "${LOG_DIR}" ]]; then
if [[ ! -d "${LOG_DIR}" ]]; then
mkdir -p "${LOG_DIR}"
fi
LOG_OPTS="--log-dir $LOG_DIR"
fi
if [[ ! -d "${CONTAINERS_DIR}" ]]; then
mkdir -p "${CONTAINERS_DIR}"
fi
if [[ "$CLEAN_DEPLOYMENTS" == "true" ]]; then
echo "Cleaning deployments directory..."
rm -rf ${DEPLOYMENTS_DIR}/*
fi
if [[ "$CLEAN_CONTAINERS" == "true" ]]; then
echo "Cleaning containers directory..."
rm -rf ${CONTAINERS_DIR}/*
fi
if [[ "$CLEAN_LOGS" == "true" ]] && [[ -n "$LOG_DIR" ]]; then
echo "Cleaning logs directory..."
rm -rf ${LOG_DIR}/*
fi
if [[ ! -d "${UPLOAD_DIRECTORY}" ]]; then
mkdir -p "${UPLOAD_DIRECTORY}"
fi
STORAGE_DRIVER="${STORAGE_DRIVER}"
if [[ -z "${STORAGE_DRIVER}" ]]; then
if [[ "true" == "`is_privileged`" ]]; then
STORAGE_DRIVER="overlay"
else
STORAGE_DRIVER="vfs"
fi
fi
echo "Using '$STORAGE_DRIVER' storage driver."
cat > /etc/containers/storage.conf <<EOF
[storage]
driver = "${STORAGE_DRIVER}"
runroot = "${CONTAINERS_DIR}/podman/run"
graphroot = "${CONTAINERS_DIR}/podman/storage"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
if [[ -n "$IMAGE_REGISTRY_CREDS" ]]; then
docker login --password "$IMAGE_REGISTRY_CREDS" --username ${IMAGE_REGISTRY_USER:-ANY} $IMAGE_REGISTRY
fi
cd /app/
while true; do
UPDATE_OPTS=""
if [[ "true" == "${ONLY_NEW_REQUESTS:-true}" ]] && [[ ! -f "${DEPLOYMENTS_DIR}/autodeploy.state" ]]; then
UPDATE_OPTS="--only-update-state"
fi
echo "=============================================================="
ps -ef | grep node | grep 'yarn start' >/dev/null
if [ $? -ne 0 ]; then
echo "########### STARTING API ############"
echo "Starting API"
yarn start &
fi
# echo "########### UNDEPLOY ############"
# laconic-so undeploy-webapp-from-registry \
# --laconic-config /etc/config/laconic.yml \
# --deployment-parent-dir "${DEPLOYMENTS_DIR}" \
# --delete-names \
# --delete-volumes \
# --state-file "${DEPLOYMENTS_DIR}/autoremove.state" \
# --include-tags "$INCLUDE_TAGS" \
# --exclude-tags "$EXCLUDE_TAGS" \
# --lrn "$LRN" \
# --min-required-payment ${MIN_REQUIRED_PAYMENT:-0} \
# $EXTRA_UNDEPLOY_OPTS \
# $UPDATE_OPTS \
# --discover
# rc=$?
# if [ $rc -eq 0 ]; then
# echo "############ UNDEPLOY SUCCESS #############"
# else
# echo "############ UNDEPLOY FAILURE STATUS $rc #############"
# fi
# echo "############ DEPLOY #############"
# laconic-so deploy-webapp-from-registry \
# --kube-config /etc/config/kube.yml \
# --laconic-config /etc/config/laconic.yml \
# --image-registry ${IMAGE_REGISTRY} \
# --deployment-parent-dir "${DEPLOYMENTS_DIR}" \
# --dns-suffix ${DEPLOYMENT_DNS_SUFFIX} \
# --record-namespace-dns lrn://${DEPLOYMENT_RECORD_NAMESPACE}/dns \
# --record-namespace-deployments lrn://${DEPLOYMENT_RECORD_NAMESPACE}/deployments \
# --state-file "${DEPLOYMENTS_DIR}/autodeploy.state" \
# --include-tags "$INCLUDE_TAGS" \
# --exclude-tags "$EXCLUDE_TAGS" \
# --fqdn-policy "${FQDN_POLICY:-prohibit}" \
# --lrn "$LRN" \
# --min-required-payment ${MIN_REQUIRED_PAYMENT:-0} \
# --config-upload-dir "$UPLOAD_DIRECTORY" \
# --private-key-file "$OPENPGP_PRIVATE_KEY_FILE" \
# --private-key-passphrase "$OPENPGP_PASSPHRASE" \
# $AUCTION_OPTS \
# $LOG_OPTS \
# $EXTRA_DEPLOY_OPTS \
# $UPDATE_OPTS \
# --discover
# rc=$?
# if [ $rc -eq 0 ]; then
# echo "############ DEPLOY SUCCESS #############"
# else
# echo "############ DEPLOY FAILURE STATUS $rc #############"
# fi
# if [ "$HANDLE_AUCTION_REQUESTS" = "true" ]; then
# echo "############ DEPLOYMENT AUCTION #############"
# laconic-so handle-deployment-auction \
# --laconic-config /etc/config/laconic.yml \
# --state-file "${DEPLOYMENTS_DIR}/autoauction.state" \
# --bid-amount ${AUCTION_BID_AMOUNT}
# rc=$?
# if [ $rc -eq 0 ]; then
# echo "############ DEPLOYMENT AUCTION SUCCESS #############"
# else
# echo "############ DEPLOYMENT AUCTION FAILURE STATUS $rc #############"
# fi
# fi
# # Cleanup any build leftovers
# if [[ "${SYSTEM_PRUNE:-false}" == "true" ]]; then
# docker system prune --all --force
# fi
# if [[ "${WEBAPP_IMAGE_PRUNE:-true}" == "true" ]]; then
# APP_IMAGES="$(docker image ls --quiet --filter 'reference=laconic-webapp')"
# DANGLING_IMAGES="$(docker image ls --quiet --filter 'dangling=true')"
# if [[ -n "$APP_IMAGES" ]] || [[ -n "$DANGLING_IMAGES" ]]; then
# echo "Pruning images: $APP_IMAGES $DANGLING_IMAGES"
# docker image rm -f $APP_IMAGES $DANGLING_IMAGES
# fi
# fi
sleep ${CHECK_INTERVAL:-15}
done
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
if [ ! -f "/etc/config/laconic.yml" ]; then
echo "/etc/config/laconic.yml is required."
exit 2
fi
if [ "$HANDLE_AUCTION_REQUESTS" = "true" ]; then
if [ -z "$AUCTION_BID_AMOUNT" ]; then
echo "AUCTION_BID_AMOUNT is required when handling auction requsts."
exit 2
fi
else
echo "Not handling auction requests"
# k8s integration only supports "always" restart policy, so wait indefinitely
# TODO: Exit container once restart policy is supported
tail -f /dev/null
fi
STORAGE_ROOT="${STORAGE_ROOT:-/srv}"
DEPLOYMENTS_DIR="${DEPLOYMENTS_DIR:-$STORAGE_ROOT/deployments}"
REGISTRY_LOCK_FILE="${REGISTRY_LOCK_FILE:-/srv/registry_mutex_lock_file}"
if [[ ! -d "${DEPLOYMENTS_DIR}" ]]; then
mkdir -p "${DEPLOYMENTS_DIR}"
fi
cd /app/
while true; do
echo "=============================================================="
echo "############ DEPLOYMENT AUCTION #############"
laconic-so handle-deployment-auction \
--laconic-config /etc/config/laconic.yml \
--registry-lock-file "${REGISTRY_LOCK_FILE}" \
--state-file "${DEPLOYMENTS_DIR}/autoauction.state" \
--bid-amount ${AUCTION_BID_AMOUNT}
rc=$?
if [ $rc -eq 0 ]; then
echo "############ DEPLOYMENT AUCTION SUCCESS #############"
else
echo "############ DEPLOYMENT AUCTION FAILURE STATUS $rc #############"
fi
sleep ${AUCTION_CHECK_INTERVAL:-10}
done
Executable
+200
View File
@@ -0,0 +1,200 @@
#!/bin/bash
if [ "$RUN_AUCTIONS_HANDLER" = "true" ]; then
exec "./scripts/handle-auctions.sh"
exit
fi
function is_privileged {
ip link add dummy0 type dummy >/dev/null
if [[ $? -eq 0 ]]; then
echo "true"
ip link delete dummy0 >/dev/null
else
echo "false"
fi
}
if [ -z "$DEPLOYMENT_DNS_SUFFIX" ]; then
echo "DEPLOYMENT_DNS_SUFFIX is required."
exit 2
fi
if [ -z "$DEPLOYMENT_RECORD_NAMESPACE" ]; then
echo "DEPLOYMENT_RECORD_NAMESPACE is required."
exit 2
fi
if [ "$FQDN_POLICY" = "allow" ] && [ -z "$DEPLOYMENT_IP" ]; then
echo "DEPLOYMENT_IP is required with 'allow' FQDN_POLICY"
exit 2
fi
if [ -z "$IMAGE_REGISTRY" ]; then
echo "IMAGE_REGISTRY is required."
exit 2
fi
if [ ! -f "/etc/config/laconic.yml" ]; then
echo "/etc/config/laconic.yml is required."
exit 2
fi
if [ ! -f "/etc/config/kube.yml" ]; then
echo "/etc/config/kube.yml is required."
exit 2
fi
AUCTION_OPTS=""
if [ "$HANDLE_AUCTION_REQUESTS" = "true" ]; then
AUCTION_OPTS="--auction-requests"
fi
STORAGE_ROOT="${STORAGE_ROOT:-/srv}"
DEPLOYMENTS_DIR="${DEPLOYMENTS_DIR:-$STORAGE_ROOT/deployments}"
LOG_DIR="${LOG_DIR:-$STORAGE_ROOT/logs}"
CONTAINERS_DIR="${CONTAINER_DIR:-$STORAGE_ROOT/containers}"
REGISTRY_LOCK_FILE="${REGISTRY_LOCK_FILE:-/srv/registry_mutex_lock_file}"
if [[ ! -d "${DEPLOYMENTS_DIR}" ]]; then
mkdir -p "${DEPLOYMENTS_DIR}"
fi
LOG_OPTS=""
if [[ -n "${LOG_DIR}" ]]; then
if [[ ! -d "${LOG_DIR}" ]]; then
mkdir -p "${LOG_DIR}"
fi
LOG_OPTS="--log-dir $LOG_DIR"
fi
if [[ ! -d "${CONTAINERS_DIR}" ]]; then
mkdir -p "${CONTAINERS_DIR}"
fi
if [[ "$CLEAN_DEPLOYMENTS" == "true" ]]; then
echo "Cleaning deployments directory..."
rm -rf ${DEPLOYMENTS_DIR}/*
fi
if [[ "$CLEAN_CONTAINERS" == "true" ]]; then
echo "Cleaning containers directory..."
rm -rf ${CONTAINERS_DIR}/*
fi
if [[ "$CLEAN_LOGS" == "true" ]] && [[ -n "$LOG_DIR" ]]; then
echo "Cleaning logs directory..."
rm -rf ${LOG_DIR}/*
fi
if [[ ! -d "${UPLOAD_DIRECTORY}" ]]; then
mkdir -p "${UPLOAD_DIRECTORY}"
fi
STORAGE_DRIVER="${STORAGE_DRIVER}"
if [[ -z "${STORAGE_DRIVER}" ]]; then
if [[ "true" == "`is_privileged`" ]]; then
STORAGE_DRIVER="overlay"
else
STORAGE_DRIVER="vfs"
fi
fi
echo "Using '$STORAGE_DRIVER' storage driver."
cat > /etc/containers/storage.conf <<EOF
[storage]
driver = "${STORAGE_DRIVER}"
runroot = "${CONTAINERS_DIR}/podman/run"
graphroot = "${CONTAINERS_DIR}/podman/storage"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
if [[ -n "$IMAGE_REGISTRY_CREDS" ]]; then
docker login --password "$IMAGE_REGISTRY_CREDS" --username ${IMAGE_REGISTRY_USER:-ANY} $IMAGE_REGISTRY
fi
cd /app/
while true; do
UPDATE_OPTS=""
if [[ "true" == "${ONLY_NEW_REQUESTS:-true}" ]] && [[ ! -f "${DEPLOYMENTS_DIR}/autodeploy.state" ]]; then
UPDATE_OPTS="--only-update-state"
fi
echo "=============================================================="
ps -ef | grep node | grep 'yarn start' >/dev/null
if [ $? -ne 0 ]; then
echo "########### STARTING API ############"
echo "Starting API"
yarn start &
fi
echo "########### UNDEPLOY ############"
laconic-so undeploy-webapp-from-registry \
--laconic-config /etc/config/laconic.yml \
--registry-lock-file "${REGISTRY_LOCK_FILE}" \
--deployment-parent-dir "${DEPLOYMENTS_DIR}" \
--delete-names \
--delete-volumes \
--state-file "${DEPLOYMENTS_DIR}/autoremove.state" \
--include-tags "$INCLUDE_TAGS" \
--exclude-tags "$EXCLUDE_TAGS" \
--lrn "$LRN" \
--min-required-payment 0 \
$EXTRA_UNDEPLOY_OPTS \
$UPDATE_OPTS \
--discover
rc=$?
if [ $rc -eq 0 ]; then
echo "############ UNDEPLOY SUCCESS #############"
else
echo "############ UNDEPLOY FAILURE STATUS $rc #############"
fi
echo "############ DEPLOY #############"
laconic-so deploy-webapp-from-registry \
--kube-config /etc/config/kube.yml \
--laconic-config /etc/config/laconic.yml \
--registry-lock-file "${REGISTRY_LOCK_FILE}" \
--image-registry ${IMAGE_REGISTRY} \
--deployment-parent-dir "${DEPLOYMENTS_DIR}" \
--dns-suffix ${DEPLOYMENT_DNS_SUFFIX} \
--record-namespace-dns lrn://${DEPLOYMENT_RECORD_NAMESPACE}/dns \
--record-namespace-deployments lrn://${DEPLOYMENT_RECORD_NAMESPACE}/deployments \
--state-file "${DEPLOYMENTS_DIR}/autodeploy.state" \
--include-tags "$INCLUDE_TAGS" \
--exclude-tags "$EXCLUDE_TAGS" \
--fqdn-policy "${FQDN_POLICY:-prohibit}" \
--ip "${DEPLOYMENT_IP}" \
--lrn "$LRN" \
--min-required-payment ${MIN_REQUIRED_PAYMENT:-0} \
--config-upload-dir "$UPLOAD_DIRECTORY" \
--private-key-file "$OPENPGP_PRIVATE_KEY_FILE" \
--private-key-passphrase "$OPENPGP_PASSPHRASE" \
--recreate-on-deploy \
$AUCTION_OPTS \
$LOG_OPTS \
$EXTRA_DEPLOY_OPTS \
$UPDATE_OPTS \
--discover
rc=$?
if [ $rc -eq 0 ]; then
echo "############ DEPLOY SUCCESS #############"
else
echo "############ DEPLOY FAILURE STATUS $rc #############"
fi
# Cleanup any build leftovers
if [[ "${SYSTEM_PRUNE:-false}" == "true" ]]; then
docker system prune --all --force
fi
if [[ "${WEBAPP_IMAGE_PRUNE:-true}" == "true" ]]; then
APP_IMAGES="$(docker image ls --quiet --filter 'reference=laconic-webapp')"
DANGLING_IMAGES="$(docker image ls --quiet --filter 'dangling=true')"
if [[ -n "$APP_IMAGES" ]] || [[ -n "$DANGLING_IMAGES" ]]; then
echo "Pruning images: $APP_IMAGES $DANGLING_IMAGES"
docker image rm -f $APP_IMAGES $DANGLING_IMAGES
fi
fi
sleep ${CHECK_INTERVAL:-15}
done
+178
View File
@@ -0,0 +1,178 @@
import axios from 'axios';
import { Config } from './config.js';
// Store verified transaction hashes to prevent double-spending
const verifiedTransactions = new Map<string, {
timestamp: number,
amount: number,
sender: string,
used: boolean
}>();
// Periodically clean up old verified transactions (older than 24 hours)
setInterval(() => {
const now = Date.now();
for (const [hash, data] of verifiedTransactions.entries()) {
if (now - data.timestamp > 24 * 60 * 60 * 1000) {
verifiedTransactions.delete(hash);
}
}
}, 60 * 60 * 1000); // Clean up every hour
/**
* Verifies a Cosmos ATOM payment transaction
* @param txHash The transaction hash to verify
* @param requiredAmount The minimum required amount of ATOM
* @param markAsUsed Set to true to mark this transaction as used (preventing double-spending)
* @returns An object with verification result and details
*/
export async function verifyAtomPayment(
txHash: string,
requiredAmount = Config.MIN_ATOM_PAYMENT,
markAsUsed = false,
timeWindowMinutes = 5
): Promise<{
valid: boolean,
reason?: string,
amount?: string,
sender?: string,
alreadyUsed?: boolean
}> {
if (!Config.ATOM_PAYMENT_ADDRESS) {
return {
valid: false,
reason: 'ATOM_PAYMENT_ADDRESS not configured'
};
}
if (!Config.COSMOS_RPC_ENDPOINT) {
return {
valid: false,
reason: 'COSMOS_RPC_ENDPOINT not configured'
};
}
// Check if we've already verified this transaction
if (verifiedTransactions.has(txHash)) {
const txData = verifiedTransactions.get(txHash);
// If the transaction is marked as used and we're not just checking
if (txData.used && !markAsUsed) {
return {
valid: false,
reason: 'Transaction has already been used for a deployment',
alreadyUsed: true
};
}
// If we're marking as used
if (markAsUsed) {
txData.used = true;
}
// Return cached verification result
return {
valid: true,
amount: `${txData.amount}uatom`,
sender: txData.sender,
alreadyUsed: txData.used
};
}
try {
// Fetch transaction from the Cosmos RPC endpoint
const response = await axios.get(
`${Config.COSMOS_RPC_ENDPOINT}/cosmos/tx/v1beta1/txs/${txHash}`
);
if (!response.data || !response.data.tx || !response.data.tx_response) {
return {
valid: false,
reason: 'Invalid transaction data from RPC endpoint'
};
}
// Check if transaction was successful
const txResponse = response.data.tx_response;
if (txResponse.code !== 0) {
return {
valid: false,
reason: `Transaction failed with code ${txResponse.code}: ${txResponse.raw_log}`
};
}
// Check transaction timestamp (5-minute window)
const txTimestamp = new Date(txResponse.timestamp);
const now = new Date();
const timeDiffMs = now.getTime() - txTimestamp.getTime();
const timeWindowMs = timeWindowMinutes * 60 * 1000;
if (timeDiffMs > timeWindowMs) {
return {
valid: false,
reason: `Transaction is older than ${timeWindowMinutes} minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
};
}
// Extract the payment details
const tx = response.data.tx;
let foundValidPayment = false;
let paymentAmountUAtom = '';
let sender = '';
// Get the sender address from the first signer
if (tx.auth_info && tx.auth_info.signer_infos && tx.auth_info.signer_infos.length > 0) {
sender = tx.auth_info.signer_infos[0].public_key.address || '';
}
// Find the send message in the transaction
for (const msg of tx.body.messages) {
if (msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') {
if (msg.to_address === Config.ATOM_PAYMENT_ADDRESS) {
for (const coin of msg.amount) {
if (coin.denom === 'uatom') {
// Get the amount in uatom
paymentAmountUAtom = coin.amount;
// Extract the required amount as an integer from the format like "10uatom"
const requiredAmountUAtom = parseInt(requiredAmount.replace(/[^0-9]/g, ''));
if (parseInt(paymentAmountUAtom) >= requiredAmountUAtom) {
foundValidPayment = true;
}
break;
}
}
}
}
}
if (!foundValidPayment) {
return {
valid: false,
reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${requiredAmount}) or not sent to the correct address`
};
}
// Cache the verification result
verifiedTransactions.set(txHash, {
timestamp: Date.now(),
amount: parseInt(paymentAmountUAtom),
sender,
used: markAsUsed
});
return {
valid: true,
amount: `${paymentAmountUAtom}uatom`,
sender,
alreadyUsed: markAsUsed
};
} catch (error) {
console.error('Error verifying ATOM payment:', error);
return {
valid: false,
reason: `Failed to verify transaction: ${error.message || 'Unknown error'}`
};
}
}
+5 -1
View File
@@ -1,7 +1,7 @@
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';
import {Registry} from '@cerc-io/laconic-sdk';
import {Registry} from '@cerc-io/registry-sdk';
const loadConfigFile = (configFilePath: string): any => {
const resolvedFilePath = path.resolve(process.cwd(), configFilePath);
@@ -22,6 +22,10 @@ export const Config = {
UPLOAD_MAX_SIZE: process.env.BUILD_LOGS || '1MB',
OPENPGP_PASSPHRASE: process.env.OPENPGP_PASSPHRASE,
OPENPGP_PRIVATE_KEY_FILE: process.env.OPENPGP_PRIVATE_KEY_FILE,
// Cosmos ATOM payment configuration
ATOM_PAYMENT_ADDRESS: process.env.ATOM_PAYMENT_ADDRESS || '',
MIN_ATOM_PAYMENT: process.env.MIN_ATOM_PAYMENT || '1000000uatom',
COSMOS_RPC_ENDPOINT: process.env.COSMOS_RPC_ENDPOINT || '',
};
export const getRegistry = (): Registry => {
+48 -44
View File
@@ -1,5 +1,5 @@
import {getRegistry} from './config.js';
import {Registry} from '@cerc-io/laconic-sdk';
import {Registry} from '@cerc-io/registry-sdk';
import stringify from 'json-stable-stringify';
import {createHash} from 'crypto';
@@ -25,10 +25,12 @@ export class RequestStatus {
public url?: string;
public deployment?: string;
public lastUpdate?: string;
public atomPaymentVerified?: boolean;
constructor(public id: string, public createTime: string,) {
this.lastState = 'SUBMITTED';
this.lastUpdate = this.createTime;
this.atomPaymentVerified = false;
}
}
@@ -70,14 +72,18 @@ export class RegHelper {
} else {
const records = await this.registry.getRecordsByIds([id]);
for (const r of records) {
this.cache.set(r.id, r);
if (Array.isArray(r.names)) {
for (const n of r.names) {
this.cache.set(n, r);
// Check if record is not null before accessing its properties
if (r && r.id) {
this.cache.set(r.id, r);
if (Array.isArray(r.names)) {
for (const n of r.names) {
this.cache.set(n, r);
}
}
}
}
return records[0];
// Return the first non-null record, or null if none found
return records.find(r => r !== null) || null;
}
}
@@ -107,90 +113,91 @@ export class RegHelper {
return [...records];
}
async deploymentRequestStatus(requestId?: string) {
console.log('Starting deploymentRequestStatus with requestId:', requestId);
// ATOM payment verification moved to background payment processor
// This method is no longer used to avoid blocking the status API
async deploymentRequestStatus(requestId?: string) {
const requests: any[] = [];
const deployments: any[] = [];
// Querying for removal requests
const removalRequests = await this.queryRecords({
type: 'ApplicationDeploymentRemovalRequest',
});
// WebappDeployer records no longer needed for status display
// Payment verification is handled by background processor
if (requestId) {
const request = await this.getRecordById(requestId);
if (request) {
requests.push(request);
console.log('Found request:', request);
} else {
console.log('Request not found for requestId:', requestId);
}
const foundDeployments = await this.queryRecords({
type: 'ApplicationDeploymentRecord',
request: requestId
});
deployments.push(...foundDeployments);
deployments.push(...await this.queryRecords({
type: 'ApplicationDeploymentRecord', request: requestId
}));
} else {
console.log('Fetching all ApplicationDeploymentRequests');
const allRequests = await this.queryRecords({
requests.push(...await this.queryRecords({
type: 'ApplicationDeploymentRequest',
});
requests.push(...allRequests);
console.log('All requests:', allRequests);
console.log('Fetching all ApplicationDeploymentRecords');
const allDeployments = await this.queryRecords({
}));
deployments.push(...await this.queryRecords({
type: 'ApplicationDeploymentRecord',
});
deployments.push(...allDeployments);
console.log('All deployments:', allDeployments);
}));
}
console.log('Sorting requests by createTime');
requests.sort((a, b) => a.createTime === b.createTime ? 0 : a.createTime > b.createTime ? 1 : -1,);
requests.reverse();
console.log('Sorted requests:', requests);
const deploymentsByRequest = new Map<string, any>();
for (const d of deployments) {
deploymentsByRequest.set(d.attributes.request, d);
if (d && d.attributes?.request) {
deploymentsByRequest.set(d.attributes.request, d);
}
}
console.log('Deployments by request:', deploymentsByRequest);
const removalsByRequest = new Map<string, any>();
for (const rr of removalRequests) {
if (rr.attributes.request) {
if (rr && rr.attributes?.request) {
removalsByRequest.set(rr.attributes.request, rr);
}
}
console.log('Removals by request:', removalsByRequest);
const latestByHostname = new Map<string, any>();
const ret = [];
for (const r of requests) {
console.log('Processing request:', r.id);
// Skip null or invalid requests
if (!r || !r.id || !r.createTime) {
console.warn('Skipping invalid request:', r);
continue;
}
const status = new RequestStatus(r.id, r.createTime);
ret.push(status);
// Skip if application attribute is missing
if (!r.attributes?.application) {
status.lastState = 'ERROR';
continue;
}
const app = await this.getRecord(r.attributes.application);
if (!app) {
console.log('Error: Application not found for request:', r.id);
status.lastState = 'ERROR';
continue;
}
status.app = r.attributes.application;
const hostname = r.attributes.dns ?? generateHostnameForApp(app);
console.log('Hostname for app:', hostname);
// Payment verification is now handled by the background payment processor
// No longer blocking the status API with external verification calls
if (r.attributes.payment) {
// Just indicate that there is a payment hash, actual verification is done in background
status.atomPaymentVerified = false; // Will be updated by payment processor if needed
}
if (deploymentsByRequest.has(r.id)) {
const deployment = deploymentsByRequest.get(r.id);
status.url = deployment.attributes.url;
status.lastUpdate = deployment.createTime;
console.log('Deployment found for request:', r.id, 'with deployment:', deployment);
if (!latestByHostname.has(hostname)) {
latestByHostname.set(hostname, status);
@@ -205,13 +212,11 @@ export class RegHelper {
}
if (removalsByRequest.has(r.id)) {
console.log('Removal request found for request:', r.id);
status.lastState = 'CANCELLED';
continue;
}
if (latestByHostname.has(hostname)) {
console.log('Cancellation found for hostname:', hostname);
status.lastState = 'CANCELLED';
continue;
}
@@ -219,7 +224,6 @@ export class RegHelper {
latestByHostname.set(hostname, status);
}
console.log('Final status array:', ret);
return ret;
}
+55 -1
View File
@@ -5,7 +5,9 @@ import {existsSync, readdirSync, readFileSync} from 'fs';
import {Config} from './config.js';
import {RegHelper} from './deployments.js';
import { Uploader } from './upload.js';
import {Uploader} from './upload.js';
import {verifyAtomPayment} from './atomPayments.js';
import {PaymentProcessor} from './paymentProcessor.js';
const app = express();
app.use(express.json());
@@ -112,6 +114,53 @@ app.post('/upload/config', configUploadParser, async (req, res) => {
}
});
// API endpoint to verify ATOM payments (with timeout to prevent blocking)
app.post('/verify/atom-payment', async (req, res) => {
try {
const { txHash, minAmount, markAsUsed } = req.body;
if (!txHash) {
return res.status(400).json({
valid: false,
reason: 'Transaction hash is required'
});
}
if (!Config.ATOM_PAYMENT_ADDRESS) {
return res.status(400).json({
valid: false,
reason: 'ATOM_PAYMENT_ADDRESS not configured'
});
}
if (!Config.COSMOS_RPC_ENDPOINT) {
return res.status(400).json({
valid: false,
reason: 'COSMOS_RPC_ENDPOINT not configured'
});
}
const requiredAmount = minAmount || Config.MIN_ATOM_PAYMENT;
// Add timeout to prevent blocking
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Verification timeout')), 10000)
);
const verificationPromise = verifyAtomPayment(txHash, requiredAmount, Boolean(markAsUsed), 5);
const result = await Promise.race([verificationPromise, timeoutPromise]);
res.json(result);
} catch (e) {
console.error('Error verifying ATOM payment:', e);
res.status(500).json({
valid: false,
reason: `Server error: ${e.message || 'Unknown error'}`
});
}
});
// deprecated
app.get('/log/:id', async (req, res) => {
try {
@@ -128,6 +177,11 @@ app.get('/log/:id', async (req, res) => {
}
});
// Start the payment processor service
const paymentProcessor = new PaymentProcessor();
paymentProcessor.start();
app.listen(Config.LISTEN_PORT, Config.LISTEN_ADDR, () => {
console.log(`listening on ${Config.LISTEN_ADDR}:${Config.LISTEN_PORT}`);
console.log('Payment processor service started');
});
+350
View File
@@ -0,0 +1,350 @@
import { Registry, Account, parseGasAndFees, DEFAULT_GAS_ESTIMATION_MULTIPLIER } from '@cerc-io/registry-sdk';
import { DeliverTxResponse } from '@cosmjs/stargate';
import { Config } from './config.js';
import { verifyAtomPayment } from './atomPayments.js';
import { RegHelper } from './deployments.js';
import yaml from 'js-yaml';
import fs from 'fs';
interface ChainConfig {
name: string;
rpcUrl: string;
chainId: string;
minAmount: string;
paymentAddress: string;
}
interface VerificationResult {
valid: boolean;
reason?: string;
amount?: string;
sender?: string;
chainUsed?: string;
}
class PaymentProcessor {
private regHelper: RegHelper;
private processedHashes: Set<string> = new Set();
private checkInterval: number = 30000; // 30 seconds
private lastProcessedTime: Date = new Date();
constructor() {
this.regHelper = new RegHelper();
// Only process requests from the last 10 minutes on startup
this.lastProcessedTime = new Date(Date.now() - 10 * 60 * 1000);
}
/**
* Start the payment processor service
*/
start(): void {
console.log('Payment processor started');
this.initializeChains();
this.processPayments();
}
/**
* Main processing loop
*/
private async processPayments(): Promise<void> {
try {
await this.processNewRequests();
} catch (error) {
console.error('Error in payment processing:', error);
}
// Schedule next run
setTimeout(() => this.processPayments(), this.checkInterval);
}
/**
* Process new ApplicationDeploymentRequest records (only recent ones)
*/
private async processNewRequests(): Promise<void> {
try {
// Query for all ApplicationDeploymentRequest records
const allRequests = await this.regHelper.queryRecords({
type: 'ApplicationDeploymentRequest',
});
// Filter to only process recent requests (created after lastProcessedTime)
const recentRequests = allRequests.filter(request => {
const createTime = new Date(request.createTime);
return createTime > this.lastProcessedTime;
});
console.log(`Found ${allRequests.length} total deployment requests, processing ${recentRequests.length} recent ones`);
for (const request of recentRequests) {
await this.processRequest(request);
}
// Update lastProcessedTime to now
this.lastProcessedTime = new Date();
} catch (error) {
console.error('Error querying deployment requests:', error);
}
}
/**
* Process individual deployment request
*/
private async processRequest(request: any): Promise<void> {
const requestId = request.id;
const paymentHash = request.attributes.payment;
// Skip if no payment hash
if (!paymentHash) {
return;
}
// Skip if already processed
if (this.processedHashes.has(paymentHash)) {
return;
}
// Skip if request already has a deployment (already successfully processed)
const existingDeployments = await this.regHelper.queryRecords({
type: 'ApplicationDeploymentRecord',
request: requestId
});
if (existingDeployments.length > 0) {
console.log(`Skipping request ${requestId} - already has deployment`);
this.processedHashes.add(paymentHash);
return;
}
console.log(`Processing request ${requestId} with payment hash ${paymentHash}`);
try {
// First check if this is an LNT payment
const lntVerification = await this.verifyLNTPayment(paymentHash, request);
if (lntVerification.valid) {
console.log(`LNT payment verified for request ${requestId}`);
this.processedHashes.add(paymentHash);
return;
}
// If LNT verification failed, try ATOM verification
const atomVerification = await this.verifyATOMPayment(paymentHash, request);
if (atomVerification.valid) {
console.log(`ATOM payment verified for request ${requestId}, converting to LNT`);
// Send LNT from deployer account to deployer account
await this.sendLNTToDeployer(request);
this.processedHashes.add(paymentHash);
return;
}
// If both failed, log and continue (don't mark as processed to allow retries)
console.log(`Payment verification failed for request ${requestId}: LNT: ${lntVerification.reason}, ATOM: ${atomVerification.reason}`);
} catch (error) {
console.error(`Error processing request ${requestId}:`, error);
}
}
/**
* Verify LNT payment - delegated to stack-orchestrator
* Since stack-orchestrator already handles LNT verification efficiently,
* we only need to handle ATOM verification here
*/
private async verifyLNTPayment(_txHash: string, _request: any): Promise<VerificationResult> {
try {
// LNT verification is handled by stack-orchestrator
// We assume if it's not an ATOM payment, stack-orchestrator will handle it
return {
valid: false,
reason: 'LNT verification delegated to stack-orchestrator'
};
} catch (error) {
return {
valid: false,
reason: `LNT verification error: ${error.message}`
};
}
}
/**
* Verify ATOM payment with 5-minute time window
*/
private async verifyATOMPayment(txHash: string, request: any): Promise<VerificationResult> {
try {
// Skip if ATOM payment not configured
if (!Config.ATOM_PAYMENT_ADDRESS || !Config.COSMOS_RPC_ENDPOINT) {
return {
valid: false,
reason: 'ATOM payment verification not configured'
};
}
// Get the deployer record to check ATOM payment config
const deployer = await this.regHelper.getRecord(request.attributes.deployer);
if (!deployer) {
return {
valid: false,
reason: 'Deployer record not found'
};
}
const atomPaymentAddress = deployer.attributes.atomPaymentAddress || Config.ATOM_PAYMENT_ADDRESS;
const minAtomPayment = deployer.attributes.minimumAtomPayment || Config.MIN_ATOM_PAYMENT;
if (!atomPaymentAddress) {
return {
valid: false,
reason: 'ATOM payment address not configured for this deployer'
};
}
// Use existing ATOM verification with 5-minute time window
const result = await verifyAtomPayment(txHash, minAtomPayment, false);
if (result.valid) {
return {
valid: true,
amount: result.amount,
sender: result.sender,
chainUsed: 'ATOM'
};
} else {
return {
valid: false,
reason: result.reason || 'ATOM payment verification failed'
};
}
} catch (error) {
return {
valid: false,
reason: `ATOM verification error: ${error.message}`
};
}
}
/**
* Send LNT from deployer account to deployer account to satisfy stack-orchestrator
*/
private async sendLNTToDeployer(request: any): Promise<void> {
try {
// Get the deployer record
const deployer = await this.regHelper.getRecord(request.attributes.deployer);
if (!deployer) {
throw new Error('Deployer record not found');
}
const paymentAddress = deployer.attributes.paymentAddress;
const minPayment = deployer.attributes.minimumPayment || '0alnt';
// Extract amount number from string like "100alnt"
const amount = minPayment.replace(/[^0-9]/g, '') || '0';
console.log(`Sending ${minPayment} LNT from deployer to deployer (${paymentAddress}) for request ${request.id}`);
// Send LNT tokens using proper Registry SDK methods
const result = await this.sendLNTTokens(paymentAddress, amount);
console.log(`LNT transfer successful: ${result.transactionHash}`);
} catch (error) {
console.error(`Error sending LNT for request ${request.id}:`, error);
throw error;
}
}
/**
* Send LNT tokens using Registry SDK
*/
private async sendLNTTokens(toAddress: string, amount: string): Promise<DeliverTxResponse> {
// Load laconic config to get registry details and private key
const configData = yaml.load(fs.readFileSync(Config.LACONIC_CONFIG, 'utf8')) as any;
const registryConfig = configData.services?.registry;
if (!registryConfig) {
throw new Error('Registry configuration not found');
}
// Get private key from environment or config
const privateKey = process.env.REGISTRY_USER_KEY || registryConfig.privateKey;
if (!privateKey) {
throw new Error('Registry private key not configured');
}
// Create account
const account = new Account(Buffer.from(privateKey, 'hex'));
await account.init();
// Create registry instance
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.rpcEndpoint,
{ chainId: registryConfig.chainId }
);
// Get laconic client
const laconicClient = await registry.getLaconicClient(account);
// Calculate fees
const fee = registryConfig.gas && registryConfig.fees ?
parseGasAndFees(registryConfig.gas, registryConfig.fees) :
DEFAULT_GAS_ESTIMATION_MULTIPLIER;
// Send tokens from deployer account to deployer account
const txResponse: DeliverTxResponse = await laconicClient.sendTokens(
account.address, // from (deployer account)
toAddress, // to (same deployer account)
[{
denom: 'alnt', // LNT token denomination
amount // amount as string
}],
fee
);
return txResponse;
}
/**
* Get chain configurations for future extensibility
*/
private getChainConfigs(): ChainConfig[] {
const configs: ChainConfig[] = [];
// ATOM configuration
if (Config.ATOM_PAYMENT_ADDRESS && Config.COSMOS_RPC_ENDPOINT) {
configs.push({
name: 'ATOM',
rpcUrl: Config.COSMOS_RPC_ENDPOINT,
chainId: 'cosmoshub-4', // Could be made configurable
minAmount: Config.MIN_ATOM_PAYMENT,
paymentAddress: Config.ATOM_PAYMENT_ADDRESS,
});
}
// Future chains can be added here
// if (Config.OSMO_PAYMENT_ADDRESS && Config.OSMO_RPC_ENDPOINT) {
// configs.push({
// name: 'OSMO',
// rpcUrl: Config.OSMO_RPC_ENDPOINT,
// chainId: 'osmosis-1',
// minAmount: Config.MIN_OSMO_PAYMENT,
// paymentAddress: Config.OSMO_PAYMENT_ADDRESS,
// });
// }
console.log(`Configured ${configs.length} payment chains`);
return configs;
}
/**
* Initialize chain configurations on startup
*/
private initializeChains(): void {
const configs = this.getChainConfigs();
console.log(`Payment processor initialized with ${configs.length} supported chains`);
}
}
export { PaymentProcessor };