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..bb0ff1fa 100644 --- a/src/chain-operations/canton/clientConfig.js +++ b/src/chain-operations/canton/clientConfig.js @@ -1,7 +1,7 @@ -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 config = new TransferAgentConfig(false); const client = new FairmintClient(config); export { config, client }; 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