Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc53cae044 | ||
|
|
84fd528619 | ||
|
|
fa4628a153 | ||
|
|
aa27f4ff18 | ||
|
|
15187227e9 | ||
|
|
429bb62cf1 | ||
|
|
a8be189229 | ||
|
|
50b1877e8c | ||
|
|
6322ebd30f | ||
|
|
7f83bc6c89 | ||
|
|
48af6dea2c | ||
|
|
cad9406579 | ||
|
|
16276e80d0 | ||
|
|
05ae7fb9db | ||
|
|
fbdeba9eed | ||
|
|
3372ce29f3 | ||
|
|
30b349ea49 |
+1
-1
@@ -10,4 +10,4 @@ SYSTEM_PRUNE=false
|
||||
WEBAPP_IMAGE_PRUNE=true
|
||||
CHECK_INTERVAL=5
|
||||
FQDN_POLICY="allow"
|
||||
DEPLOYER_GITHUB_TOKEN="optional, set for deploying private repos"
|
||||
DEPLOYMENT_IP="k8s.cluster.ip.address"
|
||||
+5
-2
@@ -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"]
|
||||
|
||||
@@ -1,113 +1,175 @@
|
||||
# node-typescript-boilerplate
|
||||
# webapp-deployment-status-api
|
||||
|
||||
[![Sponsor][sponsor-badge]][sponsor]
|
||||
[![TypeScript version][ts-badge]][typescript-5-3]
|
||||
[![Node.js version][nodejs-badge]][nodejs]
|
||||
[![APLv2][license-badge]][license]
|
||||
[![Build Status - GitHub Actions][gha-badge]][gha-ci]
|
||||
This API provides status information about webapp deployment requests and a mechanism for upload encrypted configuration
|
||||
files used by those requests. It supports payments in both Laconic testnet (alnt) and Cosmos mainnet (ATOM).
|
||||
|
||||
👩🏻💻 Developer Ready: A comprehensive template. Works out of the box for most [Node.js][nodejs] projects.
|
||||
## Build and Run
|
||||
|
||||
🏃🏽 Instant Value: All basic tools included and configured:
|
||||
|
||||
- [TypeScript][typescript] [5.3][typescript-5-3]
|
||||
- [ESM][esm]
|
||||
- [ESLint][eslint] with some initial rules recommendation
|
||||
- [Jest][jest] for fast unit testing and code coverage
|
||||
- Type definitions for Node.js and Jest
|
||||
- [Prettier][prettier] to enforce consistent code style
|
||||
- NPM [scripts](#available-scripts) for common operations
|
||||
- [EditorConfig][editorconfig] for consistent coding style
|
||||
- Reproducible environments thanks to [Volta][volta]
|
||||
- Example configuration for [GitHub Actions][gh-actions]
|
||||
- Simple example of TypeScript code and unit test
|
||||
|
||||
🤲 Free as in speech: available under the APLv2 license.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is intended to be used with the latest Active LTS release of [Node.js][nodejs].
|
||||
|
||||
### Use as a repository template
|
||||
|
||||
To start, just click the **[Use template][repo-template-action]** link (or the green button). Start adding your code in the `src` and unit tests in the `__tests__` directories.
|
||||
|
||||
### Clone repository
|
||||
|
||||
To clone the repository, use the following commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jsynowiec/node-typescript-boilerplate
|
||||
cd node-typescript-boilerplate
|
||||
npm install
|
||||
```bash
|
||||
yarn
|
||||
yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Download latest release
|
||||
## API Endpoints
|
||||
|
||||
Download and unzip the current **main** branch or one of the tags:
|
||||
### 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)
|
||||
|
||||
```sh
|
||||
wget https://github.com/jsynowiec/node-typescript-boilerplate/archive/main.zip -O node-typescript-boilerplate.zip
|
||||
unzip node-typescript-boilerplate.zip && rm node-typescript-boilerplate.zip
|
||||
### 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
|
||||
|
||||
Configuration files are encrypted prior to being uploaded using an RSA `publicKey` specified in the `WebappDeployer` record.
|
||||
On upload, the configuration is temporarily decrypted for validation, but stored in its encrypted format.
|
||||
|
||||
To create and export a key in the necessary format use:
|
||||
|
||||
```bash
|
||||
# Create a key
|
||||
gpg --batch --passphrase "SECRET" --quick-generate-key webapp-deployer-api.my.domain.com default default never
|
||||
|
||||
# Export the public key
|
||||
gpg --export webapp-deployer-api.my.domain.com > webapp-deployer-api.my.domain.com.pgp.pub
|
||||
|
||||
# Export the private key
|
||||
gpg --export-secret-keys webapp-deployer-api.my.domain.com > webapp-deployer-api.my.domain.com.pgp.key
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
### Create the Deployer Record
|
||||
|
||||
- `clean` - remove coverage data, Jest cache and transpiled files,
|
||||
- `prebuild` - lint source files and tests before building,
|
||||
- `build` - transpile TypeScript to ES6,
|
||||
- `build:watch` - interactive watch mode to automatically transpile source files,
|
||||
- `lint` - lint source files and tests,
|
||||
- `prettier` - reformat files,
|
||||
- `test` - run tests,
|
||||
- `test:watch` - interactive watch mode to automatically re-run tests
|
||||
Every webapp deployer should have `WebappDeployer` record in the registry which looks something like:
|
||||
|
||||
## Additional Information
|
||||
```yml
|
||||
record:
|
||||
type: WebappDeployer
|
||||
version: 1.0.0
|
||||
name: webapp-deployer-api.my.domain.com
|
||||
apiUrl: https://webapp-deployer-api.my.domain.com
|
||||
minimumPayment: 100alnt
|
||||
paymentAddress: laconic1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
|
||||
publicKey: mQGNBGbJUk0BDAC3j3CiaVtoEf1jrgtsjJnTA5u1a3BExP72mv0eE8y84TgY5rVcf ...
|
||||
atomPaymentAddress: cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
|
||||
minimumAtomPayment: 1
|
||||
```
|
||||
|
||||
### Why include Volta
|
||||
This record can most easily be created using `laconic-so publish-deployer-to-registry`.
|
||||
|
||||
[Volta][volta]’s toolchain always keeps track of where you are, it makes sure the tools you use always respect the settings of the project you’re working on. This means you don’t have to worry about changing the state of your installed software when switching between projects. For example, it's [used by engineers at LinkedIn][volta-tomdale] to standardize tools and have reproducible development environments.
|
||||
```bash
|
||||
laconic-so publish-deployer-to-registry \
|
||||
--laconic-config ~/.laconic/registry.yml \
|
||||
--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 \
|
||||
--atom-payment-address cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry \
|
||||
--min-atom-payment 1
|
||||
```
|
||||
|
||||
I recommend to [install][volta-getting-started] Volta and use it to manage your project's toolchain.
|
||||
This will create the record in the proper format and assign its LRN.
|
||||
|
||||
### ES Modules
|
||||
### Publish Deployment Auction
|
||||
|
||||
This template uses native [ESM][esm]. Make sure to read [this][nodejs-esm], and [this][ts47-esm] first.
|
||||
Users can optionally create an auction for app deployment with desired number of providers and max price they are willing to pay for a deployment:
|
||||
|
||||
If your project requires CommonJS, you will have to [convert to ESM][sindresorhus-esm].
|
||||
```bash
|
||||
laconic-so publish-deployment-auction \
|
||||
--laconic-config ./config.yml \
|
||||
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
|
||||
--commits-duration 3600 \
|
||||
--reveals-duration 3600 \
|
||||
--commit-fee 10000 \
|
||||
--reveal-fee 10000 \
|
||||
--max-price 5000000 \
|
||||
--num-providers 3
|
||||
```
|
||||
|
||||
Please do not open issues for questions regarding CommonJS or ESM on this repo.
|
||||
This will create a `provider` auction with given params and publish a deployment auction record.
|
||||
|
||||
## Backers & Sponsors
|
||||
### Request Deployment
|
||||
|
||||
Support this project by becoming a [sponsor][sponsor].
|
||||
Users can now request deployment using the LRN of the deployer. This will allow them to:
|
||||
|
||||
## License
|
||||
1. Discover the API URL for config uploads.
|
||||
1. Obtain the public key for encrypting config.
|
||||
1. See the minimum required payment (both alnt and ATOM if configured).
|
||||
|
||||
Licensed under the APLv2. See the [LICENSE](https://github.com/jsynowiec/node-typescript-boilerplate/blob/main/LICENSE) file for details.
|
||||
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).
|
||||
|
||||
[ts-badge]: https://img.shields.io/badge/TypeScript-5.3-blue.svg
|
||||
[nodejs-badge]: https://img.shields.io/badge/Node.js->=%2020.9-blue.svg
|
||||
[nodejs]: https://nodejs.org/dist/latest-v20.x/docs/api/
|
||||
[gha-badge]: https://github.com/jsynowiec/node-typescript-boilerplate/actions/workflows/nodejs.yml/badge.svg
|
||||
[gha-ci]: https://github.com/jsynowiec/node-typescript-boilerplate/actions/workflows/nodejs.yml
|
||||
[typescript]: https://www.typescriptlang.org/
|
||||
[typescript-5-3]: https://devblogs.microsoft.com/typescript/announcing-typescript-5-3/
|
||||
[license-badge]: https://img.shields.io/badge/license-APLv2-blue.svg
|
||||
[license]: https://github.com/jsynowiec/node-typescript-boilerplate/blob/main/LICENSE
|
||||
[sponsor-badge]: https://img.shields.io/badge/♥-Sponsor-fc0fb5.svg
|
||||
[sponsor]: https://github.com/sponsors/jsynowiec
|
||||
[jest]: https://facebook.github.io/jest/
|
||||
[eslint]: https://github.com/eslint/eslint
|
||||
[wiki-js-tests]: https://github.com/jsynowiec/node-typescript-boilerplate/wiki/Unit-tests-in-plain-JavaScript
|
||||
[prettier]: https://prettier.io
|
||||
[volta]: https://volta.sh
|
||||
[volta-getting-started]: https://docs.volta.sh/guide/getting-started
|
||||
[volta-tomdale]: https://twitter.com/tomdale/status/1162017336699838467
|
||||
[gh-actions]: https://github.com/features/actions
|
||||
[repo-template-action]: https://github.com/jsynowiec/node-typescript-boilerplate/generate
|
||||
[esm]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
|
||||
[sindresorhus-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
|
||||
[nodejs-esm]: https://nodejs.org/docs/latest-v16.x/api/esm.html
|
||||
[ts47-esm]: https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#esm-nodejs
|
||||
[editorconfig]: https://editorconfig.org
|
||||
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 \
|
||||
--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 \
|
||||
--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:
|
||||
|
||||
```bash
|
||||
laconic-so request-webapp-deployment \
|
||||
--laconic-config ~/.laconic/registry.yml \
|
||||
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
|
||||
--env-file hello.env \
|
||||
--auction-id 4c9701c22651e143202e991056b6e7649853acc5bc0e97e3a98e09c9f3355909
|
||||
```
|
||||
|
||||
This creates deployment requests targeted towards all the deployers who have won the auction. Similar to requests with payments, the config is automatically encrypted and uploaded to all the deployers.
|
||||
|
||||
### Request Undeployment
|
||||
|
||||
Users can also request removal of an existing deployment using the deployment record id:
|
||||
|
||||
```bash
|
||||
laconic-so request-webapp-undeployment \
|
||||
--laconic-config ~/.laconic/registry.yml \
|
||||
--deployer lrn://laconic/deployers/webapp-deployer-api.my.domain.com \
|
||||
--deployment bafyreigeopr72dmp6rhvnomgdz3cljbqzhh75epcrigit7ue6i6vjullme \
|
||||
--make-payment auto
|
||||
```
|
||||
|
||||
### Example Config
|
||||
|
||||
```bash
|
||||
UPLOAD_DIRECTORY="/srv/uploads/config"
|
||||
UPLOAD_MAX_SIZE="1MB"
|
||||
DEPLOYER_STATE="/srv/deployments/autodeploy.state"
|
||||
UNDEPLOYER_STATE="/srv/deployments/autoundeploy.state"
|
||||
BUILD_LOGS="/srv/logs"
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
+9
-3
@@ -36,15 +36,21 @@
|
||||
"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",
|
||||
"express-serve-static-core": "^0.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify": "^1.1.1",
|
||||
"tslib": "~2.6"
|
||||
"openpgp": "^5.11.2",
|
||||
"tslib": "~2.6",
|
||||
"yaml": "^2.5.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.10.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
Executable
+47
@@ -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
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/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
|
||||
@@ -20,6 +25,11 @@ if [ -z "$DEPLOYMENT_RECORD_NAMESPACE" ]; then
|
||||
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
|
||||
@@ -35,10 +45,16 @@ if [ ! -f "/etc/config/kube.yml" ]; then
|
||||
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}"
|
||||
@@ -71,6 +87,10 @@ if [[ "$CLEAN_LOGS" == "true" ]] && [[ -n "$LOG_DIR" ]]; then
|
||||
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
|
||||
@@ -112,20 +132,30 @@ while true; do
|
||||
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} \
|
||||
@@ -135,10 +165,24 @@ while true; do
|
||||
--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
|
||||
@@ -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'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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);
|
||||
@@ -13,11 +13,19 @@ export const Config = {
|
||||
LISTEN_PORT: parseInt(process.env.LISTEN_PORT || '9555'),
|
||||
LISTEN_ADDR: process.env.LISTEN_ADDR || '0.0.0.0',
|
||||
LACONIC_CONFIG: process.env.LACONIC_CONFIG || '/etc/config/laconic.yml',
|
||||
UPLOAD_DIRECTORY: process.env.UPLOAD_DIRECTORY || '/srv/uploads',
|
||||
DEPLOYER_STATE:
|
||||
process.env.DEPLOYER_STATE || '/srv/deployments/autodeploy.state',
|
||||
UNDEPLOYER_STATE:
|
||||
process.env.UNDEPLOYER_STATE || '/srv/deployments/autoundeploy.state',
|
||||
BUILD_LOGS: process.env.BUILD_LOGS || '/srv/logs',
|
||||
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 => {
|
||||
|
||||
+53
-20
@@ -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,12 +113,18 @@ export class RegHelper {
|
||||
return [...records];
|
||||
}
|
||||
|
||||
// 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[] = [];
|
||||
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);
|
||||
@@ -136,11 +148,13 @@ export class RegHelper {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -149,18 +163,44 @@ export class RegHelper {
|
||||
|
||||
const ret = [];
|
||||
for (const r of requests) {
|
||||
// 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) {
|
||||
status.lastState = 'ERROR';
|
||||
continue;
|
||||
}
|
||||
|
||||
status.app = r.attributes.application;
|
||||
const hostname = r.attributes.dns ?? generateHostnameForApp(app);
|
||||
|
||||
// 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;
|
||||
const shortHost = new URL(status.url).host.split('.').shift();
|
||||
if (!latestByHostname.has(shortHost)) {
|
||||
latestByHostname.set(shortHost, status);
|
||||
|
||||
if (!latestByHostname.has(hostname)) {
|
||||
latestByHostname.set(hostname, status);
|
||||
}
|
||||
status.deployment = deployment.names ? deployment.names[0] : null;
|
||||
if (status.deployment) {
|
||||
@@ -176,19 +216,12 @@ export class RegHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
const app = await this.getRecord(r.attributes.application);
|
||||
if (!app) {
|
||||
status.lastState = 'ERROR';
|
||||
continue;
|
||||
}
|
||||
|
||||
const shortHost = r.attributes.dns ?? generateHostnameForApp(app);
|
||||
if (latestByHostname.has(shortHost)) {
|
||||
if (latestByHostname.has(hostname)) {
|
||||
status.lastState = 'CANCELLED';
|
||||
continue;
|
||||
}
|
||||
|
||||
latestByHostname.set(shortHost, status);
|
||||
latestByHostname.set(hostname, status);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
+71
@@ -1,13 +1,20 @@
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import {existsSync, readdirSync, readFileSync} from 'fs';
|
||||
|
||||
import {Config} from './config.js';
|
||||
|
||||
import {RegHelper} from './deployments.js';
|
||||
import {Uploader} from './upload.js';
|
||||
import {verifyAtomPayment} from './atomPayments.js';
|
||||
import {PaymentProcessor} from './paymentProcessor.js';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const configUploader = new Uploader(Config.UPLOAD_DIRECTORY);
|
||||
const configUploadParser = bodyParser.raw({limit: Config.UPLOAD_MAX_SIZE, type: "*/*"})
|
||||
|
||||
app.use(function (_req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept',);
|
||||
@@ -95,6 +102,65 @@ app.get('/:id/log', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/upload/config', configUploadParser, async (req, res) => {
|
||||
try {
|
||||
const id = await configUploader.upload(req.body);
|
||||
res.json({
|
||||
id
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
@@ -111,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');
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,91 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import assert from 'node:assert';
|
||||
import openpgp from 'openpgp';
|
||||
import YAML from 'yaml';
|
||||
import { atob } from 'node:buffer';
|
||||
|
||||
import { Config } from './config.js';
|
||||
|
||||
let privateKey: openpgp.PrivateKey | null = null;
|
||||
|
||||
const loadPrivateKey = async () => {
|
||||
if (null == privateKey) {
|
||||
privateKey = await openpgp.decryptKey({
|
||||
privateKey: await openpgp.readPrivateKey({
|
||||
binaryKey: fs.readFileSync(Config.OPENPGP_PRIVATE_KEY_FILE)
|
||||
}),
|
||||
passphrase: Config.OPENPGP_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
const randomId = (): string =>
|
||||
crypto
|
||||
.randomUUID({ disableEntropyCache: true })
|
||||
.replaceAll('-', '')
|
||||
.toUpperCase();
|
||||
|
||||
const validateConfig = (obj): undefined => {
|
||||
assert(obj.authorized, "'authorized' is required");
|
||||
assert(Array.isArray(obj.authorized), "'authorized' must be an array");
|
||||
assert(obj.authorized.length >= 1, "'authorized' cannot be empty");
|
||||
assert(obj.config, "'config' is required");
|
||||
};
|
||||
|
||||
export const b64ToBytes = (base64): Uint8Array => {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const decrypt = async (binaryMessage: Uint8Array): Promise<any> => {
|
||||
const message = await openpgp.readMessage({
|
||||
binaryMessage,
|
||||
});
|
||||
|
||||
const { data } = await openpgp.decrypt({
|
||||
message,
|
||||
decryptionKeys: await loadPrivateKey(),
|
||||
});
|
||||
|
||||
const config = data.toString();
|
||||
return config.charAt(0) === '{' ? JSON.parse(config) : YAML.parse(config);
|
||||
};
|
||||
|
||||
export class Uploader {
|
||||
directory: string;
|
||||
|
||||
constructor(dir: string) {
|
||||
this.directory = dir;
|
||||
}
|
||||
|
||||
async upload(body: string | Uint8Array): Promise<string> {
|
||||
let raw: any;
|
||||
try {
|
||||
raw = b64ToBytes(body);
|
||||
} catch {
|
||||
raw = body;
|
||||
}
|
||||
|
||||
// We decrypt only to make sure the content is valid.
|
||||
// Once we know it is good, we want to store the encrypted copy.
|
||||
const obj = await decrypt(raw);
|
||||
validateConfig(obj);
|
||||
|
||||
let id: string;
|
||||
let destination: string;
|
||||
do {
|
||||
id = randomId();
|
||||
destination = `${this.directory}/${id}`;
|
||||
} while (fs.existsSync(destination));
|
||||
|
||||
console.log(`Wrote config to: ${destination}`);
|
||||
fs.writeFileSync(destination, raw);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": false,
|
||||
"strictNullChecks": false
|
||||
"strictNullChecks": false,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*", "__tests__/**/*"]
|
||||
}
|
||||
|
||||
@@ -1146,6 +1146,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@octetstream/promisify/-/promisify-2.0.2.tgz#29ac3bd7aefba646db670227f895d812c1a19615"
|
||||
integrity sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg==
|
||||
|
||||
"@openpgp/web-stream-tools@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@openpgp/web-stream-tools/-/web-stream-tools-0.1.3.tgz#a9750f12a634b5a15e711b6c1de559511fb53732"
|
||||
integrity sha512-mT/ds43cH6c+AO5RFpxs+LkACr7KjC3/dZWHrP6KPrWJu4uJ/XJ+p7telaoYiqUfdjiiIvdNSOfhezW9fkmboQ==
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
@@ -1657,6 +1662,16 @@ array-union@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
asn1.js@^5.0.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||
dependencies:
|
||||
bn.js "^4.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
axios@^0.26.1:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
|
||||
@@ -1787,7 +1802,7 @@ blakejs@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
|
||||
integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
|
||||
|
||||
bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
|
||||
bn.js@^4.0.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
@@ -1815,6 +1830,24 @@ body-parser@1.20.1:
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body-parser@^1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -2051,7 +2084,7 @@ content-disposition@0.5.4:
|
||||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4:
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
@@ -3916,6 +3949,13 @@ onetime@^5.1.2:
|
||||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
openpgp@^5.11.2:
|
||||
version "5.11.2"
|
||||
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.11.2.tgz#2c035a26b13feb3b0bb5180718ec91c8e65cc686"
|
||||
integrity sha512-f8dJFVLwdkvPvW3VPFs6q9Vs2+HNhdvwls7a/MIFcQUB+XiQzRe7alfa3RtwfGJU7oUDDMAWPZ0nYsHa23Az+A==
|
||||
dependencies:
|
||||
asn1.js "^5.0.0"
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
|
||||
@@ -4159,6 +4199,16 @@ raw-body@2.5.1:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react-is@^18.0.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
@@ -4272,7 +4322,7 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -4464,7 +4514,16 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -4489,7 +4548,14 @@ string_decoder@^1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -4754,7 +4820,16 @@ wif@^2.0.6:
|
||||
dependencies:
|
||||
bs58check "<3.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -4805,6 +4880,11 @@ yallist@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d"
|
||||
integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==
|
||||
|
||||
yargs-parser@^21.0.1, yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
|
||||
Reference in New Issue
Block a user