Skip to content
Merged

Dev #175

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/deploy.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ jobs:
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
Expand Down Expand Up @@ -121,14 +127,25 @@ jobs:
-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
wait_for_health "\$CONTAINER_NAME" && \
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
"
12 changes: 9 additions & 3 deletions .github/workflows/deploy.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
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
Expand Down Expand Up @@ -123,9 +126,12 @@ jobs:
-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_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-prod:${DEPLOY_TIME} && \

Expand Down
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@
[submodule "chain/lib/diamond-3-hardhat"]
path = chain/lib/diamond-3-hardhat
url = https://github.com/mudgen/diamond-3-hardhat
[submodule "fairmint-canton"]
path = src/chain-operations/canton/lib/fairmint-canton
url = https://github.com/Fairmint/canton
4 changes: 2 additions & 2 deletions src/chain-operations/canton/clientConfig.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config";
import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient";
import { TransferAgentConfig } from "./lib/config";
import { FairmintClient } from "./lib/fairmintClient";

const config = new TransferAgentConfig(true);
const client = new FairmintClient(config);
Expand Down
270 changes: 270 additions & 0 deletions src/chain-operations/canton/lib/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
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: [
{
type: "CanActAs",
party: partyId
},
{
type: "CanReadAs",
party: partyId
}
]
},
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
);
}
}
35 changes: 35 additions & 0 deletions src/chain-operations/canton/lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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`);
}
}
}
1 change: 0 additions & 1 deletion src/chain-operations/canton/lib/fairmint-canton
Submodule fairmint-canton deleted from 9be911
Loading