From 5ed920286d38e5fca63a4140dff54d5ec36ada28 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 18 Jun 2025 07:48:21 -0400 Subject: [PATCH 1/4] Restore submodule --- .gitmodules | 3 + src/chain-operations/canton/clientConfig.js | 4 +- src/chain-operations/canton/lib/client.js | 281 ------------------ src/chain-operations/canton/lib/config.js | 35 --- .../canton/lib/fairmint-canton | 1 + .../canton/lib/fairmintClient.js | 153 ---------- src/chain-operations/canton/lib/types.js | 58 ---- 7 files changed, 6 insertions(+), 529 deletions(-) delete mode 100644 src/chain-operations/canton/lib/client.js delete mode 100644 src/chain-operations/canton/lib/config.js create mode 160000 src/chain-operations/canton/lib/fairmint-canton delete mode 100644 src/chain-operations/canton/lib/fairmintClient.js delete mode 100644 src/chain-operations/canton/lib/types.js diff --git a/.gitmodules b/.gitmodules index 53046645..dc57c84c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "chain/lib/diamond-3-hardhat"] path = chain/lib/diamond-3-hardhat url = https://github.com/mudgen/diamond-3-hardhat +[submodule "src/chain-operations/canton/lib/fairmint-canton"] + path = src/chain-operations/canton/lib/fairmint-canton + url = https://github.com/Fairmint/canton.git diff --git a/src/chain-operations/canton/clientConfig.js b/src/chain-operations/canton/clientConfig.js index a5eab7fd..62299357 100644 --- a/src/chain-operations/canton/clientConfig.js +++ b/src/chain-operations/canton/clientConfig.js @@ -1,5 +1,5 @@ -import { TransferAgentConfig } from "./lib/config"; -import { FairmintClient } from "./lib/fairmintClient"; +import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config"; +import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; const config = new TransferAgentConfig(true); const client = new FairmintClient(config); diff --git a/src/chain-operations/canton/lib/client.js b/src/chain-operations/canton/lib/client.js deleted file mode 100644 index eb1c062b..00000000 --- a/src/chain-operations/canton/lib/client.js +++ /dev/null @@ -1,281 +0,0 @@ -import axios from 'axios'; -import { TransferAgentConfig } from './config.js'; -import * as fs from 'fs'; -import * as path from 'path'; - -export class TransferAgentClient { - constructor(config) { - this.config = config; - this.bearerToken = null; - this.sequenceNumber = 1; - this.axiosInstance = axios.create(); - this.logDir = path.join(process.cwd(), 'logs'); - if (!fs.existsSync(this.logDir)) { - fs.mkdirSync(this.logDir, { recursive: true }); - } - } - - getFairmintPartyId() { - return this.config.fairmintPartyId; - } - - async logRequestResponse(url, request, response) { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const logFile = path.join(this.logDir, `request-${timestamp}.json`); - - const logData = { - timestamp, - url, - request, - response - }; - - fs.writeFileSync(logFile, JSON.stringify(logData, null, 2)); - } - - async makePostRequest(url, data, headers) { - try { - const response = await this.axiosInstance.post(url, data, { headers }); - await this.logRequestResponse(url, data, response.data); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - const errorData = error.response?.data; - - // Check for security-sensitive error - if (errorData?.cause === "A security-sensitive error has been received") { - // Clear the bearer token to force re-authentication - this.bearerToken = null; - - // Get new headers with fresh authentication - const newHeaders = await this.getHeaders(); - - // Retry the request once with new authentication - try { - const retryResponse = await this.axiosInstance.post(url, data, { headers: newHeaders }); - await this.logRequestResponse(url, data, retryResponse.data); - return retryResponse.data; - } catch (retryError) { - // If retry fails, log and throw the original error - await this.logRequestResponse(url, data, { - error: axios.isAxiosError(retryError) ? retryError.response?.data || retryError.message : retryError - }); - throw error; - } - } - - await this.logRequestResponse(url, data, { - error: errorData || error.message - }); - throw error; - } - throw error; - } - } - - async authenticate() { - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', this.config.clientId); - formData.append('client_secret', this.config.clientSecret); - formData.append('audience', this.config.audience); - formData.append('scope', this.config.scope); - - try { - const response = await this.makePostRequest( - this.config.authUrl, - formData.toString(), - { - 'Content-Type': 'application/x-www-form-urlencoded', - } - ); - - this.bearerToken = response.access_token; - return this.bearerToken; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(`Authentication failed: ${error.response?.data || error.message}`); - } - throw error; - } - } - - async getHeaders() { - if (!this.bearerToken) { - await this.authenticate(); - } - - return { - 'Authorization': `Bearer ${this.bearerToken}`, - 'Content-Type': 'application/json', - }; - } - - async createCommand(params) { - const command = { - commands: [{ - CreateCommand: { - templateId: params.templateId, - createArguments: params.createArguments, - }, - }], - commandId: this.sequenceNumber.toString(), - actAs: params.actAs, - }; - - this.sequenceNumber++; - - try { - const headers = await this.getHeaders(); - const response = await this.makePostRequest( - `${this.config.ledgerUrl}/commands/submit-and-wait-for-transaction-tree`, - command, - headers - ); - - return { - contractId: response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':0'].CreatedTreeEvent.value.contractId, - updateId: response.transactionTree.updateId - }; - } catch (error) { - if (axios.isAxiosError(error)) { - const errorData = error.response?.data ? JSON.stringify(error.response.data, null, 2) : error.message; - throw new Error(`Failed to create command: ${errorData}`); - } - throw error; - } - } - - async exerciseCommand(params) { - const command = { - commands: [{ - ExerciseCommand: { - templateId: params.templateId, - contractId: params.contractId, - choice: params.choice, - choiceArgument: params.choiceArgument - } - }], - commandId: this.sequenceNumber.toString(), - actAs: params.actAs - }; - - this.sequenceNumber++; - - try { - const headers = await this.getHeaders(); - const response = await this.makePostRequest( - `${this.config.ledgerUrl}/commands/submit-and-wait-for-transaction-tree`, - command, - headers - ); - return response; - } catch (error) { - if (axios.isAxiosError(error)) { - const errorData = error.response?.data ? JSON.stringify(error.response.data, null, 2) : error.message; - throw new Error(`Failed to exercise command: ${errorData}`); - } - throw error; - } - } - - async createParty(partyIdHint) { - try { - const headers = await this.getHeaders(); - const response = await this.makePostRequest( - `${this.config.ledgerUrl}/parties`, - { - partyIdHint: `FM:${partyIdHint}`, - displayName: partyIdHint, - identityProviderId: "" - }, - headers - ); - - const partyId = response.partyDetails.party; - - // Set user rights for the newly created party - await this.setUserRights(partyId); - - return {partyId, isNewParty: true}; - } catch (error) { - if (axios.isAxiosError(error)) { - const errorData = error.response?.data; - // Check if this is a "party already exists" error - if (errorData?.cause?.includes('Party already exists')) { - // Look up the party ID from the ledger - const parties = await this.getParties(); - const existingParty = parties.partyDetails.find(p => p.party.startsWith(`FM:${partyIdHint}`)); - if (existingParty) { - // Set user rights for the newly created party - await this.setUserRights(existingParty.party); - - return { partyId: existingParty.party, isNewParty: false }; - } - } - const errorMessage = errorData ? JSON.stringify(errorData, null, 2) : error.message; - throw new Error(`Failed to create party: ${errorMessage}`); - } - throw error; - } - } - - async setUserRights(partyId) { - const headers = await this.getHeaders(); - await this.makePostRequest( - `${this.config.ledgerUrl}/users/${this.config.fairmintUserId}/rights`, - { - userId: this.config.fairmintUserId, - rights: [ - { - kind: { - "CanActAs": { - value: { - party: partyId - } - } - }, - }, - { - kind: { - "CanReadAs": { - value: { - party: partyId - } - } - }, - } - ], - identityProviderId: "" - }, - headers - ); - } - - async getParties() { - const headers = await this.getHeaders(); - return await this.makePostRequest( - `${this.config.ledgerUrl}/parties`, - {}, - headers - ); - } - - async getEventsByContractId(contractId) { - const headers = await this.getHeaders(); - return await this.makePostRequest( - `${this.config.ledgerUrl}/events/contract/${contractId}`, - {}, - headers - ); - } - - async getTransactionTreeByOffset(offset) { - const headers = await this.getHeaders(); - return await this.makePostRequest( - `${this.config.ledgerUrl}/transactions/tree/${offset}`, - {}, - headers - ); - } -} \ No newline at end of file diff --git a/src/chain-operations/canton/lib/config.js b/src/chain-operations/canton/lib/config.js deleted file mode 100644 index 5b7bdabb..00000000 --- a/src/chain-operations/canton/lib/config.js +++ /dev/null @@ -1,35 +0,0 @@ -import dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - -export class TransferAgentConfig { - constructor(isMainnet = false) { - this.authUrl = 'https://auth.transfer-agent.xyz/application/o/token/'; - this.scope = 'daml_ledger_apia'; - - if (isMainnet) { - this.ledgerUrl = 'https://ledger-api.validator.transfer-agent.xyz/v2'; - this.clientId = this.audience = 'validator-mainnet-m2m'; - this.clientSecret = process.env.TRANSFER_AGENT_MAINNET_CLIENT_SECRET || ''; - this.fairmintPartyId = process.env.FAIRMINT_MAINNET_PARTY_ID || ''; - this.fairmintUserId = process.env.FAIRMINT_MAINNET_USER_ID || ''; - } else { - this.ledgerUrl = 'https://ledger-api.validator.devnet.transfer-agent.xyz/v2'; - this.clientId = this.audience = 'validator-devnet-m2m'; - this.clientSecret = process.env.TRANSFER_AGENT_DEVNET_CLIENT_SECRET || ''; - this.fairmintPartyId = process.env.FAIRMINT_DEVNET_PARTY_ID || ''; - this.fairmintUserId = process.env.FAIRMINT_DEVNET_USER_ID || ''; - } - - if (!this.clientSecret) { - throw new Error(`${isMainnet ? 'TRANSFER_AGENT_MAINNET_CLIENT_SECRET' : 'TRANSFER_AGENT_DEVNET_CLIENT_SECRET'} environment variable is not set`); - } - if (!this.fairmintPartyId) { - throw new Error(`${isMainnet ? 'FAIRMINT_MAINNET_PARTY_ID' : 'FAIRMINT_DEVNET_PARTY_ID'} environment variable is not set`); - } - if (!this.fairmintUserId) { - throw new Error(`${isMainnet ? 'FAIRMINT_MAINNET_USER_ID' : 'FAIRMINT_DEVNET_USER_ID'} environment variable is not set`); - } - } -} \ No newline at end of file diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton new file mode 160000 index 00000000..038ba275 --- /dev/null +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -0,0 +1 @@ +Subproject commit 038ba2758334e79e4d8b6ccbb17d43f9991e3a7f diff --git a/src/chain-operations/canton/lib/fairmintClient.js b/src/chain-operations/canton/lib/fairmintClient.js deleted file mode 100644 index 66e03fa2..00000000 --- a/src/chain-operations/canton/lib/fairmintClient.js +++ /dev/null @@ -1,153 +0,0 @@ -import { TransferAgentClient } from './client.js'; -import { TransferAgentConfig } from './config.js'; - -// Application specific constants -const TEMPLATES = { - FAIRMINT_ADMIN_SERVICE: '#OpenCapTable-v00:FairmintAdminService:FairmintAdminService' -}; - -export class FairmintClient { - constructor(config) { - this.client = new TransferAgentClient(config); - } - - async createFairmintAdminService() { - const response = await this.client.createCommand({ - templateId: TEMPLATES.FAIRMINT_ADMIN_SERVICE, - createArguments: { - fairmint: this.client.getFairmintPartyId(), - }, - actAs: [this.client.getFairmintPartyId()], - }); - console.debug(`Created FairmintAdminService with contract ID: ${response.contractId}`); - return response; - } - - async authorizeIssuer(contractId, issuerPartyId) { - const response = await this.client.exerciseCommand({ - templateId: TEMPLATES.FAIRMINT_ADMIN_SERVICE, - contractId, - choice: 'AuthorizeIssuer', - choiceArgument: { - issuer: issuerPartyId - }, - actAs: [this.client.getFairmintPartyId()] - }); - - // Extract the IssuerAuthorization contract ID from the response - const authorizationContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':0'].ExercisedTreeEvent.exerciseResult; - console.debug(`Successfully authorized issuer with contract ID: ${authorizationContractId}`); - return authorizationContractId; - } - - async createParty(partyIdHint) { - const response = await this.client.createParty(partyIdHint); - console.debug(`${response.isNewParty ? 'Created' : 'Reused'} party for ${partyIdHint} with ID: ${response.partyId}`); - return response; - } - - async acceptIssuerAuthorization(authorizationContractId, name, authorizedShares, issuerPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:IssuerAuthorization:IssuerAuthorization', - contractId: authorizationContractId, - choice: 'CreateIssuer', - choiceArgument: { - name, - authorizedShares: authorizedShares.toString() - }, - actAs: [issuerPartyId] - }); - - // Extract the Issuer contract ID from the response - const issuerContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - console.debug(`Successfully created issuer with contract ID: ${issuerContractId}`); - return issuerContractId; - } - - async createStockClass(issuerContractId, stockClassType, shares, issuerPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:Issuer:Issuer', - contractId: issuerContractId, - choice: 'CreateStockClass', - choiceArgument: { - stockClassType, - shares: shares.toString() - }, - actAs: [issuerPartyId] - }); - - // Extract both the StockClass and updated Issuer contract IDs from the response - const stockClassContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':2'].CreatedTreeEvent.value.contractId; - const updatedIssuerContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - console.debug(`Created stock class with contract ID: ${stockClassContractId}`); - return { stockClassContractId, updatedIssuerContractId }; - } - - async proposeIssueStock(stockClassContractId, recipientPartyId, quantity, issuerPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:StockClass:StockClass', - contractId: stockClassContractId, - choice: 'ProposeIssueStock', - choiceArgument: { - recipient: recipientPartyId, - quantity: quantity.toString() - }, - actAs: [issuerPartyId] - }); - - // Extract both the IssueStockClassProposal and updated StockClass contract IDs from the response - const proposalContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - const updatedStockClassContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':2'].CreatedTreeEvent.value.contractId; - console.debug(`Proposed stock issuance to ${recipientPartyId} with proposal ID: ${proposalContractId}`); - return { proposalContractId, updatedStockClassContractId }; - } - - async acceptIssueStockProposal(proposalContractId, recipientPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:StockClass:IssueStockClassProposal', - contractId: proposalContractId, - choice: 'AcceptIssueStockProposal', - choiceArgument: {}, - actAs: [recipientPartyId] - }); - - // Extract the StockPosition contract ID from the response - const stockPositionContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - console.debug(`${recipientPartyId} accepted stock issuance and received position with ID: ${stockPositionContractId}`); - return stockPositionContractId; - } - - async proposeTransfer(stockPositionContractId, recipientPartyId, quantity, ownerPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:StockPosition:StockPosition', - contractId: stockPositionContractId, - choice: 'ProposeTransfer', - choiceArgument: { - recipient: recipientPartyId, - quantityToTransfer: quantity.toString() - }, - actAs: [ownerPartyId] - }); - - // Extract both the TransferProposal and updated StockPosition contract IDs from the response - const transferProposalContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - const updatedStockPositionContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':2'].CreatedTreeEvent.value.contractId; - console.debug(`${ownerPartyId} proposed transfer to ${recipientPartyId} with proposal ID: ${transferProposalContractId}`); - return { transferProposalContractId, updatedStockPositionContractId }; - } - - async acceptTransfer(transferProposalContractId, recipientPartyId) { - const response = await this.client.exerciseCommand({ - templateId: '#OpenCapTable-v00:StockPosition:StockTransferProposal', - contractId: transferProposalContractId, - choice: 'AcceptTransfer', - choiceArgument: {}, - actAs: [recipientPartyId] - }); - - // Extract the new StockPosition contract ID from the response - const stockPositionContractId = response.transactionTree.eventsById['#' + response.transactionTree.updateId + ':1'].CreatedTreeEvent.value.contractId; - console.debug(`${recipientPartyId} accepted transfer and received position with ID: ${stockPositionContractId}`); - return stockPositionContractId; - } -} \ No newline at end of file diff --git a/src/chain-operations/canton/lib/types.js b/src/chain-operations/canton/lib/types.js deleted file mode 100644 index 418508ff..00000000 --- a/src/chain-operations/canton/lib/types.js +++ /dev/null @@ -1,58 +0,0 @@ -// Note: This file is kept for documentation purposes only -// The types are not used in JavaScript but are documented here for reference - -/** - * @typedef {Object} AuthResponse - * @property {string} access_token - */ - -/** - * @typedef {Object} CreateCommand - * @property {Object} CreateCommand - * @property {string} CreateCommand.templateId - * @property {Object} CreateCommand.createArguments - */ - -/** - * @typedef {Object} ExerciseCommand - * @property {Object} ExerciseCommand - * @property {string} ExerciseCommand.templateId - * @property {string} ExerciseCommand.contractId - * @property {string} ExerciseCommand.choice - * @property {Object} ExerciseCommand.choiceArgument - */ - -/** - * @typedef {CreateCommand|ExerciseCommand} Command - */ - -/** - * @typedef {Object} CommandRequest - * @property {Command[]} commands - * @property {string} commandId - * @property {string[]} actAs - */ - -/** - * @typedef {Object} CreatedTreeEvent - * @property {Object} CreatedTreeEvent - * @property {Object} CreatedTreeEvent.value - * @property {string} CreatedTreeEvent.value.contractId - */ - -/** - * @typedef {Object} TransactionTree - * @property {string} updateId - * @property {Object.} eventsById - */ - -/** - * @typedef {Object} CommandResponse - * @property {TransactionTree} transactionTree - */ - -/** - * @typedef {Object} CreateContractResponse - * @property {string} contractId - * @property {string} updateId - */ \ No newline at end of file From 8ab8b7887c4fe861289974f280177c858416e189 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 18 Jun 2025 08:02:55 -0400 Subject: [PATCH 2/4] Devnet --- src/chain-operations/canton/clientConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain-operations/canton/clientConfig.js b/src/chain-operations/canton/clientConfig.js index 62299357..bb0ff1fa 100644 --- a/src/chain-operations/canton/clientConfig.js +++ b/src/chain-operations/canton/clientConfig.js @@ -1,7 +1,7 @@ import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config"; import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; -const config = new TransferAgentConfig(true); +const config = new TransferAgentConfig(false); const client = new FairmintClient(config); export { config, client }; From 13d4bc5beee7b8e45908f5130543dcdf97f9c8da Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 18 Jun 2025 08:08:16 -0400 Subject: [PATCH 3/4] Revert deploy.dev.yaml --- .github/workflows/deploy.dev.yaml | 289 +++++++++++++----------------- 1 file changed, 129 insertions(+), 160 deletions(-) diff --git a/.github/workflows/deploy.dev.yaml b/.github/workflows/deploy.dev.yaml index 8bee9fe1..a65b8b58 100644 --- a/.github/workflows/deploy.dev.yaml +++ b/.github/workflows/deploy.dev.yaml @@ -1,165 +1,134 @@ name: Deployment (Dev) on: - push: - branches: - - dev + push: + branches: + - dev jobs: - build: - name: Build, Test, and Deploy - environment: dev - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - submodules: true - fetch-depth: 0 - - # Setup and cache dependencies - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: "18" - cache: "yarn" - - - name: Install Node Dependencies - run: yarn install --frozen-lockfile - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - - name: Run Forge Install Script - run: chmod +x setup.sh && ./setup.sh - - # Run all tests and checks - - name: Run Forge Tests - run: cd chain && forge test --summary - - - name: Run ESLint - run: yarn lint:check - - - name: Check Formatting - run: yarn format:check - - # Build and Deploy - - name: Deploy - shell: bash - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - HOST: ${{ secrets.LIGHTSAIL_INSTANCE_PUBLIC_IP_DEV }} - PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - ETHERSCAN_L2_API_KEY: ${{ secrets.ETHERSCAN_L2_API_KEY }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DATABASE_URL: ${{ secrets.DATABASE_URL }} - RPC_URL: ${{ secrets.RPC_URL }} - FAIRMINT_MAINNET_USER_ID: ${{ secrets.FAIRMINT_MAINNET_USER_ID }} - FAIRMINT_MAINNET_PARTY_ID: ${{ secrets.FAIRMINT_MAINNET_PARTY_ID }} - TRANSFER_AGENT_MAINNET_CLIENT_SECRET: ${{ secrets.TRANSFER_AGENT_MAINNET_CLIENT_SECRET }} - FAIRMINT_DEVNET_USER_ID: ${{ secrets.FAIRMINT_DEVNET_USER_ID }} - FAIRMINT_DEVNET_PARTY_ID: ${{ secrets.FAIRMINT_DEVNET_PARTY_ID }} - TRANSFER_AGENT_DEVNET_CLIENT_SECRET: ${{ secrets.TRANSFER_AGENT_DEVNET_CLIENT_SECRET }} - - run: | - # Generate timestamp for deployment - DEPLOY_TIME=$(date +%s) - echo "DEPLOY_TIME: $DEPLOY_TIME" - - # Save SSH key and set permissions - echo "$SSH_PRIVATE_KEY" > deploy_key - chmod 600 deploy_key - - # Validate SSH key format - echo "Validating SSH key format..." - ssh-keygen -l -f deploy_key || { - echo "Error: Invalid SSH key format" - exit 1 - } - - # Test SSH connection - echo "Testing SSH connection..." - ssh -i deploy_key -o StrictHostKeyChecking=no -v ubuntu@"$HOST" "echo 'SSH connection successful'" || { - echo "Error: SSH connection failed" - exit 1 - } - - # Create a temp directory for deployment - DEPLOY_DIR="/tmp/deploy-${DEPLOY_TIME}" - mkdir -p $DEPLOY_DIR - - # Copy necessary files to temp directory - echo "Preparing deployment files..." - cp -r . $DEPLOY_DIR/ - - # Sync files to server - echo "Syncing files to server..." - rsync -az --delete \ - --exclude='node_modules' \ - --exclude='.git' \ - --exclude='deploy_key' \ - --include='chain/out' \ - --include='chain/out/**' \ - -e "ssh -i deploy_key -o StrictHostKeyChecking=no" \ - $DEPLOY_DIR/ \ - ubuntu@"$HOST":/home/ubuntu/app-${DEPLOY_TIME} - - # Execute deployment on server - ssh -i deploy_key -o StrictHostKeyChecking=no ubuntu@"$HOST" " - sudo su && \ - cd /home/ubuntu/app-${DEPLOY_TIME} && \ - echo 'Building image on host...' && \ - # Source the functions - source ./scripts/docker_container_utils.sh && \ - docker build -t ocp-dev:${DEPLOY_TIME} -f Dockerfile.dev . && \ - - # Initial cleanup - echo 'Cleaning up old resources...' && \ - docker ps -q --filter 'publish=8081' | xargs -r docker rm -f && \ - docker ps -q --filter 'publish=8082' | xargs -r docker rm -f && \ - docker container prune -f && \ - docker image prune -f && \ - - # Start new container - echo 'Starting new container...' && \ - CONTAINER_NAME=ocp-dev-${DEPLOY_TIME} && \ - - # Run container - docker run --name \$CONTAINER_NAME -d \ - --health-cmd='curl -f http://localhost:8080/health || exit 1' \ - --health-interval='2s' \ - --health-retries='3' \ - --health-timeout='5s' \ - --restart always \ - -e DOCKER_ENV='true' \ - -e NODE_ENV='development' \ - -e SENTRY_DSN='${SENTRY_DSN}' \ - -e DATABASE_URL='${DATABASE_URL}' \ - -e RPC_URL='${RPC_URL}' \ - -e PORT=8080 \ - -e PRIVATE_KEY='${PRIVATE_KEY}' \ - -e ETHERSCAN_L2_API_KEY='${ETHERSCAN_L2_API_KEY}' \ - -e FAIRMINT_MAINNET_USER_ID='${FAIRMINT_MAINNET_USER_ID}' \ - -e FAIRMINT_MAINNET_PARTY_ID='${FAIRMINT_MAINNET_PARTY_ID}' \ - -e TRANSFER_AGENT_MAINNET_CLIENT_SECRET='${TRANSFER_AGENT_MAINNET_CLIENT_SECRET}' \ - -e FAIRMINT_DEVNET_USER_ID='${FAIRMINT_DEVNET_USER_ID}' \ - -e FAIRMINT_DEVNET_PARTY_ID='${FAIRMINT_DEVNET_PARTY_ID}' \ - -e TRANSFER_AGENT_DEVNET_CLIENT_SECRET='${TRANSFER_AGENT_DEVNET_CLIENT_SECRET}' \ - -v '/home/ubuntu/global-bundle.pem:/global-bundle.pem' \ - ocp-dev:${DEPLOY_TIME} && \ - - # Wait for container to be healthy - echo "Printing logs before health check:" - docker logs \$CONTAINER_NAME || true - - wait_for_health "\$CONTAINER_NAME" - if [ \$? -eq 0 ]; then - handle_container_switch "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" - else - echo "Container failed health check. Printing logs:" - docker logs \$CONTAINER_NAME || true - handle_failed_deployment "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" - fi - " + build: + name: Build, Test, and Deploy + environment: dev + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + # Setup and cache dependencies + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + cache: "yarn" + + - name: Install Node Dependencies + run: yarn install --frozen-lockfile + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Run Forge Install Script + run: chmod +x setup.sh && ./setup.sh + + # Run all tests and checks + - name: Run Forge Tests + run: cd chain && forge test --summary + + - name: Run ESLint + run: yarn lint:check + + - name: Check Formatting + run: yarn format:check + + # Build and Deploy + - name: Deploy + shell: bash + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HOST: ${{ secrets.LIGHTSAIL_INSTANCE_PUBLIC_IP_DEV }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + ETHERSCAN_L2_API_KEY: ${{ secrets.ETHERSCAN_L2_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + RPC_URL: ${{ secrets.RPC_URL }} + + run: | + # Generate timestamp for deployment + DEPLOY_TIME=$(date +%s) + echo "DEPLOY_TIME: $DEPLOY_TIME" + + # Save SSH key and set permissions + echo "$SSH_PRIVATE_KEY" > deploy_key + chmod 600 deploy_key + + # Create a temp directory for deployment + DEPLOY_DIR="/tmp/deploy-${DEPLOY_TIME}" + mkdir -p $DEPLOY_DIR + + # Copy necessary files to temp directory + echo "Preparing deployment files..." + cp -r . $DEPLOY_DIR/ + + # Sync files to server + echo "Syncing files to server..." + rsync -az --delete \ + --exclude='node_modules' \ + --exclude='.git' \ + --exclude='deploy_key' \ + --include='chain/out' \ + --include='chain/out/**' \ + -e "ssh -i deploy_key -o StrictHostKeyChecking=no" \ + $DEPLOY_DIR/ \ + ubuntu@"$HOST":/home/ubuntu/app-${DEPLOY_TIME} + + # Execute deployment on server + ssh -i deploy_key -o StrictHostKeyChecking=no ubuntu@"$HOST" " + sudo su && \ + cd /home/ubuntu/app-${DEPLOY_TIME} && \ + echo 'Building image on host...' && \ + # Source the functions + source ./scripts/docker_container_utils.sh && \ + docker build -t ocp-dev:${DEPLOY_TIME} -f Dockerfile.dev . && \ + + # Initial cleanup + echo 'Cleaning up old resources...' && \ + docker ps -q --filter 'publish=8081' | xargs -r docker rm -f && \ + docker ps -q --filter 'publish=8082' | xargs -r docker rm -f && \ + docker container prune -f && \ + docker image prune -f && \ + + # Start new container + echo 'Starting new container...' && \ + CONTAINER_NAME=ocp-dev-${DEPLOY_TIME} && \ + + # Run container + docker run --name \$CONTAINER_NAME -d \ + --health-cmd='curl -f http://localhost:8080/health || exit 1' \ + --health-interval='2s' \ + --health-retries='3' \ + --health-timeout='5s' \ + --restart always \ + -e DOCKER_ENV='true' \ + -e NODE_ENV='development' \ + -e SENTRY_DSN='${SENTRY_DSN}' \ + -e DATABASE_URL='${DATABASE_URL}' \ + -e RPC_URL='${RPC_URL}' \ + -e PORT=8080 \ + -e PRIVATE_KEY='${PRIVATE_KEY}' \ + -e ETHERSCAN_L2_API_KEY='${ETHERSCAN_L2_API_KEY}' \ + -v '/home/ubuntu/global-bundle.pem:/global-bundle.pem' \ + ocp-dev:${DEPLOY_TIME} && \ + + # Wait for container to be healthy + wait_for_health "\$CONTAINER_NAME" && \ + if [ \$? -eq 0 ]; then + handle_container_switch "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" + else + handle_failed_deployment "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" + fi + " From 2da3b19fc04723d8facfd37eb2f0d680e82758db Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 18 Jun 2025 08:16:33 -0400 Subject: [PATCH 4/4] Revert deploy.dev.yaml --- .github/workflows/deploy.dev.yaml | 289 +++++++++++++++++------------- 1 file changed, 160 insertions(+), 129 deletions(-) diff --git a/.github/workflows/deploy.dev.yaml b/.github/workflows/deploy.dev.yaml index a65b8b58..8bee9fe1 100644 --- a/.github/workflows/deploy.dev.yaml +++ b/.github/workflows/deploy.dev.yaml @@ -1,134 +1,165 @@ name: Deployment (Dev) on: - push: - branches: - - dev + push: + branches: + - dev jobs: - build: - name: Build, Test, and Deploy - environment: dev - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - submodules: true - fetch-depth: 0 - - # Setup and cache dependencies - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: "18" - cache: "yarn" - - - name: Install Node Dependencies - run: yarn install --frozen-lockfile - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - - name: Run Forge Install Script - run: chmod +x setup.sh && ./setup.sh - - # Run all tests and checks - - name: Run Forge Tests - run: cd chain && forge test --summary - - - name: Run ESLint - run: yarn lint:check - - - name: Check Formatting - run: yarn format:check - - # Build and Deploy - - name: Deploy - shell: bash - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - HOST: ${{ secrets.LIGHTSAIL_INSTANCE_PUBLIC_IP_DEV }} - PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - ETHERSCAN_L2_API_KEY: ${{ secrets.ETHERSCAN_L2_API_KEY }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DATABASE_URL: ${{ secrets.DATABASE_URL }} - RPC_URL: ${{ secrets.RPC_URL }} - - run: | - # Generate timestamp for deployment - DEPLOY_TIME=$(date +%s) - echo "DEPLOY_TIME: $DEPLOY_TIME" - - # Save SSH key and set permissions - echo "$SSH_PRIVATE_KEY" > deploy_key - chmod 600 deploy_key - - # Create a temp directory for deployment - DEPLOY_DIR="/tmp/deploy-${DEPLOY_TIME}" - mkdir -p $DEPLOY_DIR - - # Copy necessary files to temp directory - echo "Preparing deployment files..." - cp -r . $DEPLOY_DIR/ - - # Sync files to server - echo "Syncing files to server..." - rsync -az --delete \ - --exclude='node_modules' \ - --exclude='.git' \ - --exclude='deploy_key' \ - --include='chain/out' \ - --include='chain/out/**' \ - -e "ssh -i deploy_key -o StrictHostKeyChecking=no" \ - $DEPLOY_DIR/ \ - ubuntu@"$HOST":/home/ubuntu/app-${DEPLOY_TIME} - - # Execute deployment on server - ssh -i deploy_key -o StrictHostKeyChecking=no ubuntu@"$HOST" " - sudo su && \ - cd /home/ubuntu/app-${DEPLOY_TIME} && \ - echo 'Building image on host...' && \ - # Source the functions - source ./scripts/docker_container_utils.sh && \ - docker build -t ocp-dev:${DEPLOY_TIME} -f Dockerfile.dev . && \ - - # Initial cleanup - echo 'Cleaning up old resources...' && \ - docker ps -q --filter 'publish=8081' | xargs -r docker rm -f && \ - docker ps -q --filter 'publish=8082' | xargs -r docker rm -f && \ - docker container prune -f && \ - docker image prune -f && \ - - # Start new container - echo 'Starting new container...' && \ - CONTAINER_NAME=ocp-dev-${DEPLOY_TIME} && \ - - # Run container - docker run --name \$CONTAINER_NAME -d \ - --health-cmd='curl -f http://localhost:8080/health || exit 1' \ - --health-interval='2s' \ - --health-retries='3' \ - --health-timeout='5s' \ - --restart always \ - -e DOCKER_ENV='true' \ - -e NODE_ENV='development' \ - -e SENTRY_DSN='${SENTRY_DSN}' \ - -e DATABASE_URL='${DATABASE_URL}' \ - -e RPC_URL='${RPC_URL}' \ - -e PORT=8080 \ - -e PRIVATE_KEY='${PRIVATE_KEY}' \ - -e ETHERSCAN_L2_API_KEY='${ETHERSCAN_L2_API_KEY}' \ - -v '/home/ubuntu/global-bundle.pem:/global-bundle.pem' \ - ocp-dev:${DEPLOY_TIME} && \ - - # Wait for container to be healthy - wait_for_health "\$CONTAINER_NAME" && \ - if [ \$? -eq 0 ]; then - handle_container_switch "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" - else - handle_failed_deployment "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" - fi - " + build: + name: Build, Test, and Deploy + environment: dev + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + # Setup and cache dependencies + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + cache: "yarn" + + - name: Install Node Dependencies + run: yarn install --frozen-lockfile + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Run Forge Install Script + run: chmod +x setup.sh && ./setup.sh + + # Run all tests and checks + - name: Run Forge Tests + run: cd chain && forge test --summary + + - name: Run ESLint + run: yarn lint:check + + - name: Check Formatting + run: yarn format:check + + # Build and Deploy + - name: Deploy + shell: bash + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + HOST: ${{ secrets.LIGHTSAIL_INSTANCE_PUBLIC_IP_DEV }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + ETHERSCAN_L2_API_KEY: ${{ secrets.ETHERSCAN_L2_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + RPC_URL: ${{ secrets.RPC_URL }} + FAIRMINT_MAINNET_USER_ID: ${{ secrets.FAIRMINT_MAINNET_USER_ID }} + FAIRMINT_MAINNET_PARTY_ID: ${{ secrets.FAIRMINT_MAINNET_PARTY_ID }} + TRANSFER_AGENT_MAINNET_CLIENT_SECRET: ${{ secrets.TRANSFER_AGENT_MAINNET_CLIENT_SECRET }} + FAIRMINT_DEVNET_USER_ID: ${{ secrets.FAIRMINT_DEVNET_USER_ID }} + FAIRMINT_DEVNET_PARTY_ID: ${{ secrets.FAIRMINT_DEVNET_PARTY_ID }} + TRANSFER_AGENT_DEVNET_CLIENT_SECRET: ${{ secrets.TRANSFER_AGENT_DEVNET_CLIENT_SECRET }} + + run: | + # Generate timestamp for deployment + DEPLOY_TIME=$(date +%s) + echo "DEPLOY_TIME: $DEPLOY_TIME" + + # Save SSH key and set permissions + echo "$SSH_PRIVATE_KEY" > deploy_key + chmod 600 deploy_key + + # Validate SSH key format + echo "Validating SSH key format..." + ssh-keygen -l -f deploy_key || { + echo "Error: Invalid SSH key format" + exit 1 + } + + # Test SSH connection + echo "Testing SSH connection..." + ssh -i deploy_key -o StrictHostKeyChecking=no -v ubuntu@"$HOST" "echo 'SSH connection successful'" || { + echo "Error: SSH connection failed" + exit 1 + } + + # Create a temp directory for deployment + DEPLOY_DIR="/tmp/deploy-${DEPLOY_TIME}" + mkdir -p $DEPLOY_DIR + + # Copy necessary files to temp directory + echo "Preparing deployment files..." + cp -r . $DEPLOY_DIR/ + + # Sync files to server + echo "Syncing files to server..." + rsync -az --delete \ + --exclude='node_modules' \ + --exclude='.git' \ + --exclude='deploy_key' \ + --include='chain/out' \ + --include='chain/out/**' \ + -e "ssh -i deploy_key -o StrictHostKeyChecking=no" \ + $DEPLOY_DIR/ \ + ubuntu@"$HOST":/home/ubuntu/app-${DEPLOY_TIME} + + # Execute deployment on server + ssh -i deploy_key -o StrictHostKeyChecking=no ubuntu@"$HOST" " + sudo su && \ + cd /home/ubuntu/app-${DEPLOY_TIME} && \ + echo 'Building image on host...' && \ + # Source the functions + source ./scripts/docker_container_utils.sh && \ + docker build -t ocp-dev:${DEPLOY_TIME} -f Dockerfile.dev . && \ + + # Initial cleanup + echo 'Cleaning up old resources...' && \ + docker ps -q --filter 'publish=8081' | xargs -r docker rm -f && \ + docker ps -q --filter 'publish=8082' | xargs -r docker rm -f && \ + docker container prune -f && \ + docker image prune -f && \ + + # Start new container + echo 'Starting new container...' && \ + CONTAINER_NAME=ocp-dev-${DEPLOY_TIME} && \ + + # Run container + docker run --name \$CONTAINER_NAME -d \ + --health-cmd='curl -f http://localhost:8080/health || exit 1' \ + --health-interval='2s' \ + --health-retries='3' \ + --health-timeout='5s' \ + --restart always \ + -e DOCKER_ENV='true' \ + -e NODE_ENV='development' \ + -e SENTRY_DSN='${SENTRY_DSN}' \ + -e DATABASE_URL='${DATABASE_URL}' \ + -e RPC_URL='${RPC_URL}' \ + -e PORT=8080 \ + -e PRIVATE_KEY='${PRIVATE_KEY}' \ + -e ETHERSCAN_L2_API_KEY='${ETHERSCAN_L2_API_KEY}' \ + -e FAIRMINT_MAINNET_USER_ID='${FAIRMINT_MAINNET_USER_ID}' \ + -e FAIRMINT_MAINNET_PARTY_ID='${FAIRMINT_MAINNET_PARTY_ID}' \ + -e TRANSFER_AGENT_MAINNET_CLIENT_SECRET='${TRANSFER_AGENT_MAINNET_CLIENT_SECRET}' \ + -e FAIRMINT_DEVNET_USER_ID='${FAIRMINT_DEVNET_USER_ID}' \ + -e FAIRMINT_DEVNET_PARTY_ID='${FAIRMINT_DEVNET_PARTY_ID}' \ + -e TRANSFER_AGENT_DEVNET_CLIENT_SECRET='${TRANSFER_AGENT_DEVNET_CLIENT_SECRET}' \ + -v '/home/ubuntu/global-bundle.pem:/global-bundle.pem' \ + ocp-dev:${DEPLOY_TIME} && \ + + # Wait for container to be healthy + echo "Printing logs before health check:" + docker logs \$CONTAINER_NAME || true + + wait_for_health "\$CONTAINER_NAME" + if [ \$? -eq 0 ]; then + handle_container_switch "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" + else + echo "Container failed health check. Printing logs:" + docker logs \$CONTAINER_NAME || true + handle_failed_deployment "\$CONTAINER_NAME" "${DEPLOY_TIME}" "dev" + fi + "