From 57497a1b141081af747f551c2aa83b71e4bd09f2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 11:05:48 -0400 Subject: [PATCH 01/25] POC Canton demo integration --- src/chain-operations/canton/constants.js | 1 + .../canton/deployCapTableCanton.js | 19 +++++++++++++++++++ src/chain-operations/deployCapTable.js | 6 ++++++ src/examples/testTransfer.mjs | 3 ++- src/utils/chains.js | 6 ++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/chain-operations/canton/constants.js create mode 100644 src/chain-operations/canton/deployCapTableCanton.js diff --git a/src/chain-operations/canton/constants.js b/src/chain-operations/canton/constants.js new file mode 100644 index 00000000..3545da8f --- /dev/null +++ b/src/chain-operations/canton/constants.js @@ -0,0 +1 @@ +export const CANTON_CHAIN_ID = 6797110116111110; diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js new file mode 100644 index 00000000..8e374563 --- /dev/null +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -0,0 +1,19 @@ +import { TransferAgentConfig } from "../../../../canton/scripts/src/helpers/config"; +import { FairmintClient } from "../../../../canton/scripts/src/helpers/fairmintClient"; + +// eslint-disable-next-line no-unused-vars +export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId) { + const config = new TransferAgentConfig(); + const client = new FairmintClient(config); + + console.log("🗽 | Deploying cap table on Canton..."); + + // Pre-req: Create FairmintAdminService [One time] + const { contractId, updateId } = await client.createFairmintAdminService(); + console.log(`Created FairmintAdminService with contract ID: ${contractId}`); + + return { + address: contractId, + deployHash: updateId, + }; +} diff --git a/src/chain-operations/deployCapTable.js b/src/chain-operations/deployCapTable.js index 3ebdb5c3..a2708a9c 100644 --- a/src/chain-operations/deployCapTable.js +++ b/src/chain-operations/deployCapTable.js @@ -7,6 +7,8 @@ import getProvider from "./getProvider.js"; import Factory, { FACTORY_VERSION } from "../db/objects/Factory.js"; import assert from "node:assert"; import { decodeError } from "../utils/errorDecoder"; +import { CANTON_CHAIN_ID } from "./canton/constants.js"; +import { deployCapTableCanton } from "./canton/deployCapTableCanton.js"; setupEnv(); @@ -21,6 +23,10 @@ export const getWallet = async (chainId) => { }; async function deployCapTable(issuerId, initial_shares_authorized, chainId) { + if (chainId === CANTON_CHAIN_ID) { + return deployCapTableCanton(issuerId, initial_shares_authorized, chainId); + } + // Get provider for specified chain const wallet = await getWallet(chainId); console.log("🗽 | Wallet address: ", wallet.address); diff --git a/src/examples/testTransfer.mjs b/src/examples/testTransfer.mjs index d0c391aa..5e3a0863 100644 --- a/src/examples/testTransfer.mjs +++ b/src/examples/testTransfer.mjs @@ -2,6 +2,7 @@ import { issuer, stakeholder1, stakeholder2, stockClass, stockIssuance, stockTra import axios from "axios"; import sleep from "../utils/sleep.js"; import { v4 as uuid } from "uuid"; +import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants.js"; const main = async () => { try { @@ -14,7 +15,7 @@ const main = async () => { // 1. Create issuer console.log("⏳ Creating issuer..."); issuer.id = issuerId; - issuer.chain_id = 31337; + issuer.chain_id = CANTON_CHAIN_ID; const issuerResponse = await axios.post("http://localhost:8080/issuer/create", issuer); console.log("✅ Issuer created:", issuerResponse.data); diff --git a/src/utils/chains.js b/src/utils/chains.js index abca1b0b..036528dd 100644 --- a/src/utils/chains.js +++ b/src/utils/chains.js @@ -1,3 +1,5 @@ +import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants.js"; + // Chain configuration for supported networks export const SUPPORTED_CHAINS = { 8453: { @@ -18,6 +20,10 @@ export const SUPPORTED_CHAINS = { rpcUrl: "http://localhost:8545", wsUrl: "ws://localhost:8545", }, + [CANTON_CHAIN_ID]: { + // Canton + name: "Canton", + }, }; // Get chain configuration From 9e51b4ee33cab211248e1862b8f84a3dfcddc108 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 11:12:59 -0400 Subject: [PATCH 02/25] Ignores Canton chain --- src/utils/websocket.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts index 9640b025..89602f51 100644 --- a/src/utils/websocket.ts +++ b/src/utils/websocket.ts @@ -7,6 +7,7 @@ import { handleStockPlan, txMapper, txTypes } from "../chain-operations/transact import { handleStakeholder, handleStockClass } from "../chain-operations/transactionHandlers"; import Issuer from "../db/objects/Issuer"; import { TxCreated, StakeholderCreated, StockClassCreated, StockPlanCreated } from "../chain-operations/topics"; +import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants"; const TOPICS = { TxCreated, StakeholderCreated, StockClassCreated, StockPlanCreated }; @@ -42,6 +43,10 @@ export const addAddressesToWatch = async (chainId: string, addresses: string | s // Function to setup a single chain listener const setupChainListener = async (chainId: string, addresses: string[]) => { + if (chainId === CANTON_CHAIN_ID.toString()) { + // TODO: not yet supported + return; + } const provider = getChainProvider(chainId); if (addresses.length > 0) { From aafd1b6c1b9328d8cdda27f46fb5febfa53b2f8b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 12:18:45 -0400 Subject: [PATCH 03/25] Switch to submodules Created with git submodule add --name fairmint-canton --force https://github.com/Fairmint/canton src/chain-operations/canton/lib/fairmint-canton --- .gitmodules | 3 +++ .prettierignore | 3 ++- eslint.config.js | 4 ++-- src/chain-operations/canton/deployCapTableCanton.js | 4 ++-- src/chain-operations/canton/lib/fairmint-canton | 1 + 5 files changed, 10 insertions(+), 5 deletions(-) create mode 160000 src/chain-operations/canton/lib/fairmint-canton diff --git a/.gitmodules b/.gitmodules index 53046645..d5e25e1a 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 "fairmint-canton"] + path = src/chain-operations/canton/lib/fairmint-canton + url = https://github.com/Fairmint/canton diff --git a/.prettierignore b/.prettierignore index d1086455..09a9e083 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ /node_modules /chain/out .history +src/chain-operations/canton/lib # Build output /dist @@ -14,4 +15,4 @@ .env* # Logs -*.log \ No newline at end of file +*.log diff --git a/eslint.config.js b/eslint.config.js index 901bbf4c..3ed31e68 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,7 @@ import importPlugin from "eslint-plugin-import"; export default [ { files: ["**/*.{js,ts,mjs,mts}"], - ignores: ["node_modules/**", "eslint.config.js"], + ignores: ["node_modules/**", "eslint.config.js", "src/chain-operations/canton/lib/**"], languageOptions: { ecmaVersion: 2022, sourceType: "module", @@ -39,7 +39,7 @@ export default [ }, { files: ["**/*.ts"], - ignores: ["node_modules/**"], + ignores: ["node_modules/**", "src/chain-operations/canton/lib/**"], languageOptions: { parser: tsparser, parserOptions: { diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 8e374563..501389f5 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -1,5 +1,5 @@ -import { TransferAgentConfig } from "../../../../canton/scripts/src/helpers/config"; -import { FairmintClient } from "../../../../canton/scripts/src/helpers/fairmintClient"; +import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config"; +import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; // eslint-disable-next-line no-unused-vars export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId) { diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton new file mode 160000 index 00000000..56485bcd --- /dev/null +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -0,0 +1 @@ +Subproject commit 56485bcdafa2f6726269192a46da2f8ab2603046 From 6a94d283e7dd5b155d290e8883c3fcc251b46b69 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 12:47:02 -0400 Subject: [PATCH 04/25] Update submodule --- src/chain-operations/canton/lib/fairmint-canton | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton index 56485bcd..ff2aef0f 160000 --- a/src/chain-operations/canton/lib/fairmint-canton +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -1 +1 @@ -Subproject commit 56485bcdafa2f6726269192a46da2f8ab2603046 +Subproject commit ff2aef0ffbe4ef69fd4eee74981c422e32b9e37c From 2610caba125daacf0a218386f2d49c4596df8af2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 13:32:52 -0400 Subject: [PATCH 05/25] Bump dep --- src/chain-operations/canton/lib/fairmint-canton | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton index ff2aef0f..0861261d 160000 --- a/src/chain-operations/canton/lib/fairmint-canton +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -1 +1 @@ -Subproject commit ff2aef0ffbe4ef69fd4eee74981c422e32b9e37c +Subproject commit 0861261d16ba7a70875e07ae5178985f90e217ae From 9bd6b42dc487ac69bcc711eedb92148c6118996a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 14:51:41 -0400 Subject: [PATCH 06/25] Canton env config --- .env.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bec8e4db..b24f9fcf 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,9 @@ CONVERTIBLES_FACET= EQUITY_COMPENSATION_FACET= STOCK_PLAN_FACET= WARRANT_FACET= -STAKEHOLDER_NFT_FACET= \ No newline at end of file +STAKEHOLDER_NFT_FACET= + +# Canton config +TRANSFER_AGENT_CLIENT_SECRET= +FAIRMINT_PARTY_ID= +FAIRMINT_USER_ID= From 5dde47d70f5ec9fb8e707a27378e47c81d2b169a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 15:31:00 -0400 Subject: [PATCH 07/25] Stub create stakeholders --- src/chain-operations/canton/constants.js | 7 ++++++- src/chain-operations/canton/deployCapTableCanton.js | 1 - src/chain-operations/deployCapTable.js | 4 ++-- src/chain-operations/getContractInstances.js | 6 ++++++ src/chain-operations/getProvider.js | 5 +++++ src/examples/testTransfer.mjs | 4 ++-- src/routes/stakeholder/base.js | 12 +++++++++--- src/utils/chains.js | 4 ++-- src/utils/websocket.ts | 11 ++++++++--- 9 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/chain-operations/canton/constants.js b/src/chain-operations/canton/constants.js index 3545da8f..4dbdd6aa 100644 --- a/src/chain-operations/canton/constants.js +++ b/src/chain-operations/canton/constants.js @@ -1 +1,6 @@ -export const CANTON_CHAIN_ID = 6797110116111110; +export const CANTON_MAINNET_CHAIN_ID = 6765788401; +export const CANTON_DEVNET_CHAIN_ID = 6765788402; + +export function isCantonChainId(chainId) { + return chainId == CANTON_MAINNET_CHAIN_ID || chainId == CANTON_DEVNET_CHAIN_ID; +} diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 501389f5..4272505c 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -10,7 +10,6 @@ export async function deployCapTableCanton(issuerId, initial_shares_authorized, // Pre-req: Create FairmintAdminService [One time] const { contractId, updateId } = await client.createFairmintAdminService(); - console.log(`Created FairmintAdminService with contract ID: ${contractId}`); return { address: contractId, diff --git a/src/chain-operations/deployCapTable.js b/src/chain-operations/deployCapTable.js index a2708a9c..794cf46d 100644 --- a/src/chain-operations/deployCapTable.js +++ b/src/chain-operations/deployCapTable.js @@ -7,7 +7,7 @@ import getProvider from "./getProvider.js"; import Factory, { FACTORY_VERSION } from "../db/objects/Factory.js"; import assert from "node:assert"; import { decodeError } from "../utils/errorDecoder"; -import { CANTON_CHAIN_ID } from "./canton/constants.js"; +import { isCantonChainId } from "./canton/constants.js"; import { deployCapTableCanton } from "./canton/deployCapTableCanton.js"; setupEnv(); @@ -23,7 +23,7 @@ export const getWallet = async (chainId) => { }; async function deployCapTable(issuerId, initial_shares_authorized, chainId) { - if (chainId === CANTON_CHAIN_ID) { + if (isCantonChainId(chainId)) { return deployCapTableCanton(issuerId, initial_shares_authorized, chainId); } diff --git a/src/chain-operations/getContractInstances.js b/src/chain-operations/getContractInstances.js index a4afc68a..606b32c8 100644 --- a/src/chain-operations/getContractInstances.js +++ b/src/chain-operations/getContractInstances.js @@ -11,10 +11,16 @@ import WARRANT_FACET from "../../chain/out/WarrantFacet.sol/WarrantFacet.json"; import EQUITY_COMPENSATION_FACET from "../../chain/out/EquityCompensationFacet.sol/EquityCompensationFacet.json"; import STOCK_PLAN_FACET from "../../chain/out/StockPlanFacet.sol/StockPlanFacet.json"; import STAKEHOLDER_NFT_FACET from "../../chain/out/StakeholderNFTFacet.sol/StakeholderNFTFacet.json"; +import { isCantonChainId } from "./canton/constants.js"; setupEnv(); export const getContractInstance = (address, chainId) => { + if (isCantonChainId(chainId)) { + console.log(`Canton chain ${chainId} contract instance is not supported yet`); + return null; + } + const WALLET_PRIVATE_KEY = process.env.PRIVATE_KEY; // Create a combined ABI from all facets const combinedABI = [ diff --git a/src/chain-operations/getProvider.js b/src/chain-operations/getProvider.js index e361ca80..a1916042 100644 --- a/src/chain-operations/getProvider.js +++ b/src/chain-operations/getProvider.js @@ -1,7 +1,12 @@ import { ethers } from "ethers"; import { getChainConfig } from "../utils/chains.js"; +import { isCantonChainId } from "./canton/constants.js"; function getProvider(chainId) { + if (isCantonChainId(chainId)) { + throw new Error("Canton is not supported yet"); + } + const chainConfig = getChainConfig(chainId); if (!chainConfig) { throw new Error(`Unsupported chain ID: ${chainId}`); diff --git a/src/examples/testTransfer.mjs b/src/examples/testTransfer.mjs index 5e3a0863..c38a727f 100644 --- a/src/examples/testTransfer.mjs +++ b/src/examples/testTransfer.mjs @@ -2,7 +2,7 @@ import { issuer, stakeholder1, stakeholder2, stockClass, stockIssuance, stockTra import axios from "axios"; import sleep from "../utils/sleep.js"; import { v4 as uuid } from "uuid"; -import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants.js"; +import { CANTON_DEVNET_CHAIN_ID } from "../chain-operations/canton/constants.js"; const main = async () => { try { @@ -15,7 +15,7 @@ const main = async () => { // 1. Create issuer console.log("⏳ Creating issuer..."); issuer.id = issuerId; - issuer.chain_id = CANTON_CHAIN_ID; + issuer.chain_id = CANTON_DEVNET_CHAIN_ID; const issuerResponse = await axios.post("http://localhost:8080/issuer/create", issuer); console.log("✅ Issuer created:", issuerResponse.data); diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index 6a643b2b..a37ceb4e 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -12,6 +12,7 @@ import { createStakeholder } from "../../db/operations/create.js"; import { readIssuerById, readStakeholderById, getAllStakeholdersByIssuerId } from "../../db/operations/read.js"; import validateInputAgainstOCF from "../../utils/validateInputAgainstSchema.js"; import Stakeholder from "../../db/objects/Stakeholder.js"; +import { isCantonChainId } from "../../chain-operations/canton/constants.js"; const router = Router(); @@ -112,12 +113,17 @@ router.post("/create", async (req, res) => { const stakeholder = await createStakeholder(incomingStakeholderForDB); // Save onchain - const receipt = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id); - await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash: receipt.hash }); + let receipt; + if (!isCantonChainId(issuer.chain_id)) { + receipt = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id); + await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash: receipt.hash }); + } else { + // TODO: implement canton + } console.log("✅ | Stakeholder created offchain:", stakeholder); - res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash: receipt.hash } }); + res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash: receipt?.hash } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); diff --git a/src/utils/chains.js b/src/utils/chains.js index 036528dd..4f0df326 100644 --- a/src/utils/chains.js +++ b/src/utils/chains.js @@ -1,4 +1,4 @@ -import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants.js"; +import { CANTON_DEVNET_CHAIN_ID } from "../chain-operations/canton/constants.js"; // Chain configuration for supported networks export const SUPPORTED_CHAINS = { @@ -20,7 +20,7 @@ export const SUPPORTED_CHAINS = { rpcUrl: "http://localhost:8545", wsUrl: "ws://localhost:8545", }, - [CANTON_CHAIN_ID]: { + [CANTON_DEVNET_CHAIN_ID]: { // Canton name: "Canton", }, diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts index 89602f51..bcf47723 100644 --- a/src/utils/websocket.ts +++ b/src/utils/websocket.ts @@ -7,7 +7,7 @@ import { handleStockPlan, txMapper, txTypes } from "../chain-operations/transact import { handleStakeholder, handleStockClass } from "../chain-operations/transactionHandlers"; import Issuer from "../db/objects/Issuer"; import { TxCreated, StakeholderCreated, StockClassCreated, StockPlanCreated } from "../chain-operations/topics"; -import { CANTON_CHAIN_ID } from "../chain-operations/canton/constants"; +import { isCantonChainId } from "../chain-operations/canton/constants"; const TOPICS = { TxCreated, StakeholderCreated, StockClassCreated, StockPlanCreated }; @@ -28,6 +28,11 @@ const getChainProvider = (chainId: string): ethers.Provider => { // Function to add new addresses to watch for a specific chain export const addAddressesToWatch = async (chainId: string, addresses: string | string[]) => { + if (isCantonChainId(chainId)) { + console.log(`Canton chain ${chainId} address watching is not supported yet`); + return; + } + const addressArray = Array.isArray(addresses) ? addresses : [addresses]; if (!watchedAddressesByChain.has(chainId)) { @@ -43,8 +48,8 @@ export const addAddressesToWatch = async (chainId: string, addresses: string | s // Function to setup a single chain listener const setupChainListener = async (chainId: string, addresses: string[]) => { - if (chainId === CANTON_CHAIN_ID.toString()) { - // TODO: not yet supported + if (isCantonChainId(chainId)) { + console.log(`Canton chain ${chainId} chain listener is not supported yet`); return; } const provider = getChainProvider(chainId); From c17a8bef6785cf36379285f928bdd8a9f8739b98 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 16:00:37 -0400 Subject: [PATCH 08/25] Complete issuer/create POC --- .../canton/deployCapTableCanton.js | 24 +++++++++++++++---- src/chain-operations/deployCapTable.js | 4 ++-- src/routes/issuer.js | 7 +++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 4272505c..6547fc8d 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -2,17 +2,31 @@ import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/c import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; // eslint-disable-next-line no-unused-vars -export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId) { +export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId, issuer) { const config = new TransferAgentConfig(); const client = new FairmintClient(config); console.log("🗽 | Deploying cap table on Canton..."); - // Pre-req: Create FairmintAdminService [One time] - const { contractId, updateId } = await client.createFairmintAdminService(); + // Create FairmintAdminService [One time] + const { contractId } = await client.createFairmintAdminService(); + + // Create new party for issuer [Once per issuer] + const { partyId: issuerPartyId } = await client.createParty(issuerId); + + // Authorize issuer [Once per issuer] + const authorizationContractId = await client.authorizeIssuer(contractId, issuerPartyId); + + // Issuer accepts authorization [Once per issuer] + const issuerContractId = await client.acceptIssuerAuthorization( + authorizationContractId, + issuer.legal_name, + initial_shares_authorized, + issuerPartyId + ); return { - address: contractId, - deployHash: updateId, + address: issuerPartyId, + deployHash: issuerContractId, }; } diff --git a/src/chain-operations/deployCapTable.js b/src/chain-operations/deployCapTable.js index 794cf46d..7deb48a4 100644 --- a/src/chain-operations/deployCapTable.js +++ b/src/chain-operations/deployCapTable.js @@ -22,9 +22,9 @@ export const getWallet = async (chainId) => { return new ethers.Wallet(WALLET_PRIVATE_KEY, provider); }; -async function deployCapTable(issuerId, initial_shares_authorized, chainId) { +async function deployCapTable(issuerId, initial_shares_authorized, chainId, issuer) { if (isCantonChainId(chainId)) { - return deployCapTableCanton(issuerId, initial_shares_authorized, chainId); + return deployCapTableCanton(issuerId, initial_shares_authorized, chainId, issuer); } // Get provider for specified chain diff --git a/src/routes/issuer.js b/src/routes/issuer.js index d798983f..ae8cbcfd 100644 --- a/src/routes/issuer.js +++ b/src/routes/issuer.js @@ -64,7 +64,12 @@ issuer.post("/create", async (req, res) => { const issuerIdBytes16 = convertUUIDToBytes16(incomingIssuerToValidate.id); console.log("💾 | Issuer id in bytes16 ", issuerIdBytes16); - const { address, deployHash } = await deployCapTable(issuerIdBytes16, incomingIssuerToValidate.initial_shares_authorized, chain_id); + const { address, deployHash } = await deployCapTable( + issuerIdBytes16, + incomingIssuerToValidate.initial_shares_authorized, + chain_id, + incomingIssuerToValidate + ); const incomingIssuerForDB = { ...incomingIssuerToValidate, From 7c5468c7015b14fec777a6c68d5d4cd2373f54c6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 16:23:05 -0400 Subject: [PATCH 09/25] Rename var --- src/routes/stakeholder/base.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index a37ceb4e..13207e1b 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -113,17 +113,17 @@ router.post("/create", async (req, res) => { const stakeholder = await createStakeholder(incomingStakeholderForDB); // Save onchain - let receipt; + let tx_hash; if (!isCantonChainId(issuer.chain_id)) { - receipt = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id); - await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash: receipt.hash }); + ({ hash: tx_hash } = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id)); + await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash }); } else { // TODO: implement canton } console.log("✅ | Stakeholder created offchain:", stakeholder); - res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash: receipt?.hash } }); + res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 3c865225609eaef7fd07d75c1f9d7a638fdfdf70 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 10 Jun 2025 16:35:04 -0400 Subject: [PATCH 10/25] CAIP-7 TODO --- src/chain-operations/canton/constants.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/chain-operations/canton/constants.js b/src/chain-operations/canton/constants.js index 4dbdd6aa..90a5789e 100644 --- a/src/chain-operations/canton/constants.js +++ b/src/chain-operations/canton/constants.js @@ -1,3 +1,6 @@ +// TODO https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-7.md +// Once changed from chainId: number to CAIP-7: string, use `canton:mainnet` and `canton:devnet` + export const CANTON_MAINNET_CHAIN_ID = 6765788401; export const CANTON_DEVNET_CHAIN_ID = 6765788402; From 02837783b6bf95f90d514e5ac6c533baa9256465 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 10:04:56 -0400 Subject: [PATCH 11/25] Save issuerContractId --- src/chain-operations/canton/deployCapTableCanton.js | 9 +++++---- .../canton/stakeholderControllerCanton.js | 5 +++++ src/db/objects/Issuer.js | 1 + src/routes/issuer.js | 3 ++- src/routes/stakeholder/base.js | 3 ++- 5 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/chain-operations/canton/stakeholderControllerCanton.js diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 6547fc8d..0047a182 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -1,11 +1,11 @@ import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config"; import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; +const config = new TransferAgentConfig(); +const client = new FairmintClient(config); + // eslint-disable-next-line no-unused-vars export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId, issuer) { - const config = new TransferAgentConfig(); - const client = new FairmintClient(config); - console.log("🗽 | Deploying cap table on Canton..."); // Create FairmintAdminService [One time] @@ -27,6 +27,7 @@ export async function deployCapTableCanton(issuerId, initial_shares_authorized, return { address: issuerPartyId, - deployHash: issuerContractId, + // deployId: updateId, // TODO + issuerContractId, }; } diff --git a/src/chain-operations/canton/stakeholderControllerCanton.js b/src/chain-operations/canton/stakeholderControllerCanton.js new file mode 100644 index 00000000..6873b6b3 --- /dev/null +++ b/src/chain-operations/canton/stakeholderControllerCanton.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line no-unused-vars +export const convertAndReflectStakeholderOnchainCanton = async (contract, stakeholderId) => { + console.log("🗽 | Converting and reflecting stakeholder onchain Canton..."); + // TODO +}; diff --git a/src/db/objects/Issuer.js b/src/db/objects/Issuer.js index 7a2f5f93..4cf309a1 100644 --- a/src/db/objects/Issuer.js +++ b/src/db/objects/Issuer.js @@ -23,6 +23,7 @@ const IssuerSchema = new mongoose.Schema( chain_id: { type: Number, required: true }, tx_hash: { type: String, default: null }, factory: { ref: "Factory", type: String }, + issuer_contract_id: { type: String, default: null }, }, { timestamps: true } ); diff --git a/src/routes/issuer.js b/src/routes/issuer.js index ae8cbcfd..b509dd7b 100644 --- a/src/routes/issuer.js +++ b/src/routes/issuer.js @@ -64,7 +64,7 @@ issuer.post("/create", async (req, res) => { const issuerIdBytes16 = convertUUIDToBytes16(incomingIssuerToValidate.id); console.log("💾 | Issuer id in bytes16 ", issuerIdBytes16); - const { address, deployHash } = await deployCapTable( + const { address, deployHash, issuerContractId } = await deployCapTable( issuerIdBytes16, incomingIssuerToValidate.initial_shares_authorized, chain_id, @@ -76,6 +76,7 @@ issuer.post("/create", async (req, res) => { deployed_to: address, tx_hash: deployHash, chain_id, + issuer_contract_id: issuerContractId, }; const issuer = await createIssuer(incomingIssuerForDB); diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index 13207e1b..89b32d87 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -13,6 +13,7 @@ import { readIssuerById, readStakeholderById, getAllStakeholdersByIssuerId } fro import validateInputAgainstOCF from "../../utils/validateInputAgainstSchema.js"; import Stakeholder from "../../db/objects/Stakeholder.js"; import { isCantonChainId } from "../../chain-operations/canton/constants.js"; +import { convertAndReflectStakeholderOnchainCanton } from "../../chain-operations/canton/stakeholderControllerCanton.js"; const router = Router(); @@ -118,7 +119,7 @@ router.post("/create", async (req, res) => { ({ hash: tx_hash } = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id)); await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash }); } else { - // TODO: implement canton + tx_hash = await convertAndReflectStakeholderOnchainCanton(contract, incomingStakeholderForDB.id); } console.log("✅ | Stakeholder created offchain:", stakeholder); From 92e4c2bf33193c08abf591f22e1198ef47f328b5 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 10:10:56 -0400 Subject: [PATCH 12/25] Switch to partyId --- src/chain-operations/canton/deployCapTableCanton.js | 2 +- src/db/objects/Issuer.js | 2 +- src/routes/issuer.js | 4 ++-- src/routes/stakeholder/base.js | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 0047a182..33ae7822 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -26,7 +26,7 @@ export async function deployCapTableCanton(issuerId, initial_shares_authorized, ); return { - address: issuerPartyId, + partyId: issuerPartyId, // deployId: updateId, // TODO issuerContractId, }; diff --git a/src/db/objects/Issuer.js b/src/db/objects/Issuer.js index 4cf309a1..f1e259e2 100644 --- a/src/db/objects/Issuer.js +++ b/src/db/objects/Issuer.js @@ -23,7 +23,7 @@ const IssuerSchema = new mongoose.Schema( chain_id: { type: Number, required: true }, tx_hash: { type: String, default: null }, factory: { ref: "Factory", type: String }, - issuer_contract_id: { type: String, default: null }, + party_id: { type: String, default: null }, }, { timestamps: true } ); diff --git a/src/routes/issuer.js b/src/routes/issuer.js index b509dd7b..1442c43d 100644 --- a/src/routes/issuer.js +++ b/src/routes/issuer.js @@ -64,7 +64,7 @@ issuer.post("/create", async (req, res) => { const issuerIdBytes16 = convertUUIDToBytes16(incomingIssuerToValidate.id); console.log("💾 | Issuer id in bytes16 ", issuerIdBytes16); - const { address, deployHash, issuerContractId } = await deployCapTable( + const { address, deployHash, partyId } = await deployCapTable( issuerIdBytes16, incomingIssuerToValidate.initial_shares_authorized, chain_id, @@ -76,7 +76,7 @@ issuer.post("/create", async (req, res) => { deployed_to: address, tx_hash: deployHash, chain_id, - issuer_contract_id: issuerContractId, + party_id: partyId, }; const issuer = await createIssuer(incomingIssuerForDB); diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index 89b32d87..e94c945c 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -110,9 +110,6 @@ router.post("/create", async (req, res) => { }); } - // Save offchain - const stakeholder = await createStakeholder(incomingStakeholderForDB); - // Save onchain let tx_hash; if (!isCantonChainId(issuer.chain_id)) { @@ -122,6 +119,9 @@ router.post("/create", async (req, res) => { tx_hash = await convertAndReflectStakeholderOnchainCanton(contract, incomingStakeholderForDB.id); } + // Save offchain + const stakeholder = await createStakeholder(incomingStakeholderForDB); + console.log("✅ | Stakeholder created offchain:", stakeholder); res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash } }); From a9a18061d96926057d64429fc98c97bc72029400 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 10:19:52 -0400 Subject: [PATCH 13/25] Save stakeholder --- src/chain-operations/canton/clientConfig.js | 7 +++++++ src/chain-operations/canton/deployCapTableCanton.js | 6 +----- .../canton/stakeholderControllerCanton.js | 10 ++++++++-- src/db/objects/Stakeholder.js | 1 + src/routes/stakeholder/base.js | 5 +++-- 5 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 src/chain-operations/canton/clientConfig.js diff --git a/src/chain-operations/canton/clientConfig.js b/src/chain-operations/canton/clientConfig.js new file mode 100644 index 00000000..e2454373 --- /dev/null +++ b/src/chain-operations/canton/clientConfig.js @@ -0,0 +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(); +const client = new FairmintClient(config); + +export { config, client }; diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 33ae7822..86b560f6 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -1,8 +1,4 @@ -import { TransferAgentConfig } from "./lib/fairmint-canton/scripts/src/helpers/config"; -import { FairmintClient } from "./lib/fairmint-canton/scripts/src/helpers/fairmintClient"; - -const config = new TransferAgentConfig(); -const client = new FairmintClient(config); +import { client } from "./clientConfig"; // eslint-disable-next-line no-unused-vars export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId, issuer) { diff --git a/src/chain-operations/canton/stakeholderControllerCanton.js b/src/chain-operations/canton/stakeholderControllerCanton.js index 6873b6b3..e04738cf 100644 --- a/src/chain-operations/canton/stakeholderControllerCanton.js +++ b/src/chain-operations/canton/stakeholderControllerCanton.js @@ -1,5 +1,11 @@ +import { client } from "./clientConfig"; + // eslint-disable-next-line no-unused-vars -export const convertAndReflectStakeholderOnchainCanton = async (contract, stakeholderId) => { +export const convertAndReflectStakeholderOnchainCanton = async (stakeholderId) => { console.log("🗽 | Converting and reflecting stakeholder onchain Canton..."); - // TODO + + // Create new party for stakeholder [Once per stakeholder] + const { partyId } = await client.createParty(stakeholderId); + + return { partyId }; }; diff --git a/src/db/objects/Stakeholder.js b/src/db/objects/Stakeholder.js index 01cd9f52..1eb583cb 100644 --- a/src/db/objects/Stakeholder.js +++ b/src/db/objects/Stakeholder.js @@ -20,6 +20,7 @@ const StakeholderSchema = new mongoose.Schema( addresses: [{}], tax_ids: [{}], tx_hash: { type: String, default: null }, + party_id: { type: String, default: null }, }, { timestamps: true } ); diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index e94c945c..44583699 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -112,15 +112,16 @@ router.post("/create", async (req, res) => { // Save onchain let tx_hash; + let partyId; if (!isCantonChainId(issuer.chain_id)) { ({ hash: tx_hash } = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id)); await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash }); } else { - tx_hash = await convertAndReflectStakeholderOnchainCanton(contract, incomingStakeholderForDB.id); + ({ updateId: tx_hash, partyId } = await convertAndReflectStakeholderOnchainCanton(incomingStakeholderForDB.id)); } // Save offchain - const stakeholder = await createStakeholder(incomingStakeholderForDB); + const stakeholder = await createStakeholder({ ...incomingStakeholderForDB, party_id: partyId }); console.log("✅ | Stakeholder created offchain:", stakeholder); From 225a0bf0500451e678b277f1885a99ccc9b38ab3 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 10:50:40 -0400 Subject: [PATCH 14/25] Create stock class --- .../canton/deployCapTableCanton.js | 2 +- .../canton/stockClassControllerCanton.js | 13 +++++++++++++ src/controllers/stockClassController.js | 8 +++++++- src/db/objects/StockClass.js | 1 + src/routes/stockClass.js | 17 +++++++++++++---- 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 src/chain-operations/canton/stockClassControllerCanton.js diff --git a/src/chain-operations/canton/deployCapTableCanton.js b/src/chain-operations/canton/deployCapTableCanton.js index 86b560f6..cc15f101 100644 --- a/src/chain-operations/canton/deployCapTableCanton.js +++ b/src/chain-operations/canton/deployCapTableCanton.js @@ -24,6 +24,6 @@ export async function deployCapTableCanton(issuerId, initial_shares_authorized, return { partyId: issuerPartyId, // deployId: updateId, // TODO - issuerContractId, + address: issuerContractId, }; } diff --git a/src/chain-operations/canton/stockClassControllerCanton.js b/src/chain-operations/canton/stockClassControllerCanton.js new file mode 100644 index 00000000..c0af06fc --- /dev/null +++ b/src/chain-operations/canton/stockClassControllerCanton.js @@ -0,0 +1,13 @@ +import { client } from "./clientConfig"; + +export const convertAndReflectStockClassOnchainCanton = async (stockClass, issuer) => { + const classType = stockClass.class_type === "COMMON" ? "Common" : "Unknown"; + const { stockClassContractId, updatedIssuerContractId } = await client.createStockClass( + issuer.deployed_to, + classType, + stockClass.initial_shares_authorized, + issuer.party_id + ); + + return { stockClassContractId, updatedIssuerContractId }; +}; diff --git a/src/controllers/stockClassController.js b/src/controllers/stockClassController.js index 2d14c004..a30b52f5 100644 --- a/src/controllers/stockClassController.js +++ b/src/controllers/stockClassController.js @@ -1,9 +1,15 @@ import { toScaledBigNumber } from "../utils/convertToFixedPointDecimals.js"; import { convertUUIDToBytes16 } from "../utils/convertUUID.js"; import { decodeError } from "../utils/errorDecoder.js"; +import { isCantonChainId } from "../chain-operations/canton/constants.js"; +import { convertAndReflectStockClassOnchainCanton } from "../chain-operations/canton/stockClassControllerCanton.js"; /// @dev: controller handles conversion from OCF type to Onchain types and creates the stock class. -export const convertAndReflectStockClassOnchain = async (contract, stockClass) => { +export const convertAndReflectStockClassOnchain = async (contract, stockClass, issuer) => { + if (isCantonChainId(issuer.chain_id)) { + return convertAndReflectStockClassOnchainCanton(stockClass, issuer); + } + try { const stockClassIdBytes16 = convertUUIDToBytes16(stockClass.id); const scaledSharePrice = toScaledBigNumber(stockClass.price_per_share.amount); diff --git a/src/db/objects/StockClass.js b/src/db/objects/StockClass.js index 2ce98201..a4bafffe 100644 --- a/src/db/objects/StockClass.js +++ b/src/db/objects/StockClass.js @@ -25,6 +25,7 @@ const StockClassSchema = new mongoose.Schema( }, is_onchain_synced: { type: Boolean, default: false }, tx_hash: { type: String, default: null }, + contract_id: { type: String, default: null }, }, { timestamps: true } ); diff --git a/src/routes/stockClass.js b/src/routes/stockClass.js index aefc1824..179d5b0d 100644 --- a/src/routes/stockClass.js +++ b/src/routes/stockClass.js @@ -6,6 +6,7 @@ import { createStockClass } from "../db/operations/create.js"; import { readIssuerById, readStockClassById } from "../db/operations/read.js"; import validateInputAgainstOCF from "../utils/validateInputAgainstSchema.js"; import Stockclass from "../db/objects/StockClass"; +import Issuer from "../db/objects/Issuer"; const stockClass = Router(); @@ -68,12 +69,20 @@ stockClass.post("/create", async (req, res) => { const stockClass = await createStockClass(incomingStockClassForDB); // Save Onchain - const receipt = await convertAndReflectStockClassOnchain(contract, incomingStockClassForDB); - await Stockclass.findByIdAndUpdate(stockClass._id, { tx_hash: receipt.hash }); - + const { + hash: tx_hash, + stockClassContractId, + updatedIssuerContractId, + } = await convertAndReflectStockClassOnchain(contract, incomingStockClassForDB, issuer); + await Stockclass.findByIdAndUpdate(stockClass._id, { tx_hash, contract_id: stockClassContractId }); console.log("✅ | Stock Class created offchain:", stockClass); - res.status(200).send({ stockClass: { ...stockClass.toObject(), tx_hash: receipt.hash } }); + if (updatedIssuerContractId) { + await Issuer.findByIdAndUpdate(issuerId, { deployed_to: updatedIssuerContractId }); + console.log("✅ | Issuer updated offchain:", issuer); + } + + res.status(200).send({ stockClass: { ...stockClass.toObject(), tx_hash } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 522d179f44a2eebfe0031ec686a93c177042d2b6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 12:08:22 -0400 Subject: [PATCH 15/25] Create issuance --- .../issuanceControllerCanton.js | 19 ++++++++++ .../transactions/issuanceController.js | 20 ++++++++++- src/db/objects/Stakeholder.js | 1 + src/routes/transactions/base.js | 35 ++++++++++++++++--- 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/chain-operations/issuanceControllerCanton.js diff --git a/src/chain-operations/issuanceControllerCanton.js b/src/chain-operations/issuanceControllerCanton.js new file mode 100644 index 00000000..fbe5f7a8 --- /dev/null +++ b/src/chain-operations/issuanceControllerCanton.js @@ -0,0 +1,19 @@ +import { client } from "./canton/clientConfig"; + +export const convertAndCreateIssuanceStockOnchainCanton = async ({ stockClassContractId, stakeholderPartyId, quantity, issuerPartyId }) => { + // Issuer proposes quantity shares to stakeholder + const { proposalContractId, updatedStockClassContractId } = await client.proposeIssueStock( + stockClassContractId, + stakeholderPartyId, + quantity, + issuerPartyId + ); + + // Stakeholder accepts the proposal and receives shares + const stakeholderStockPositionContractId = await client.acceptIssueStockProposal(proposalContractId, stakeholderPartyId); + + return { + stakeholderStockPositionContractId, + updatedStockClassContractId, + }; +}; diff --git a/src/controllers/transactions/issuanceController.js b/src/controllers/transactions/issuanceController.js index ca1e5a41..f63fd198 100644 --- a/src/controllers/transactions/issuanceController.js +++ b/src/controllers/transactions/issuanceController.js @@ -1,12 +1,30 @@ import { convertUUIDToBytes16 } from "../../utils/convertUUID.js"; import { toScaledBigNumber } from "../../utils/convertToFixedPointDecimals.js"; import { decodeError } from "../../utils/errorDecoder.js"; +import { isCantonChainId } from "../../chain-operations/canton/constants.js"; +import { convertAndCreateIssuanceStockOnchainCanton } from "../../chain-operations/issuanceControllerCanton.js"; // Stock Issuance export const convertAndCreateIssuanceStockOnchain = async ( contract, - { id, security_id, stock_class_id, stakeholder_id, quantity, share_price, custom_id = "" } + { + id, + chain_id, + security_id, + stock_class_id, + stakeholder_id, + quantity, + share_price, + custom_id = "", + stockClassContractId, + stakeholderPartyId, + issuerPartyId, + } ) => { + if (isCantonChainId(chain_id)) { + return convertAndCreateIssuanceStockOnchainCanton({ stockClassContractId, stakeholderPartyId, quantity, issuerPartyId }); + } + try { const tx = await contract.issueStock({ id: convertUUIDToBytes16(id), diff --git a/src/db/objects/Stakeholder.js b/src/db/objects/Stakeholder.js index 1eb583cb..ff4d845a 100644 --- a/src/db/objects/Stakeholder.js +++ b/src/db/objects/Stakeholder.js @@ -21,6 +21,7 @@ const StakeholderSchema = new mongoose.Schema( tax_ids: [{}], tx_hash: { type: String, default: null }, party_id: { type: String, default: null }, + stock_position_contract_id: { type: String, default: null }, }, { timestamps: true } ); diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index ec439ae0..44bde74b 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -44,6 +44,7 @@ import { readStockPlanById, readIssuerById, readStockClassById, + readStakeholderById, readConvertibleIssuanceBySecurityId, readStockIssuanceBySecurityId, readEquityCompensationIssuanceBySecurityId, @@ -56,6 +57,7 @@ import get from "lodash/get"; import { convertAndCreateEquityCompensationExerciseOnchain } from "../../controllers/transactions/exerciseController"; import { adjustStockPlanPoolOnchain } from "../../controllers/stockPlanController"; import StockIssuance from "../../db/objects/transactions/issuance/StockIssuance.js"; +import StockClass from "../../db/objects/StockClass.js"; import ConvertibleIssuance from "../../db/objects/transactions/issuance/ConvertibleIssuance.js"; import EquityCompensationIssuance from "../../db/objects/transactions/issuance/EquityCompensationIssuance.js"; import WarrantIssuance from "../../db/objects/transactions/issuance/WarrantIssuance.js"; @@ -63,6 +65,7 @@ import StockClassAuthorizedSharesAdjustment from "../../db/objects/transactions/ import StockPlanPoolAdjustment from "../../db/objects/transactions/adjustment/StockPlanPoolAdjustment.js"; import { EquityCompensationExercise } from "../../db/objects/transactions/exercise"; import { StockCancellation } from "../../db/objects/transactions/cancellation"; +import Stakeholder from "../../db/objects/Stakeholder"; const transactions = Router(); @@ -71,7 +74,7 @@ transactions.post("/issuance/stock", async (req, res) => { const { issuerId, data } = req.body; try { - await readIssuerById(issuerId); + const issuer = await readIssuerById(issuerId); const incomingStockIssuance = { id: uuid(), // for OCF Validation security_id: uuid(), // for OCF Validation @@ -90,8 +93,14 @@ transactions.post("/issuance/stock", async (req, res) => { } const stockIssuance = await createStockIssuance({ ...incomingStockIssuance, issuer: issuerId }); - - const receipt = await convertAndCreateIssuanceStockOnchain(contract, { + const stockClass = await readStockClassById(incomingStockIssuance.stock_class_id); + const stakeholder = await readStakeholderById(incomingStockIssuance.stakeholder_id); + + const { + hash: tx_hash, + stakeholderStockPositionContractId, + updatedStockClassContractId, + } = await convertAndCreateIssuanceStockOnchain(contract, { security_id: incomingStockIssuance.security_id, stock_class_id: incomingStockIssuance.stock_class_id, stakeholder_id: incomingStockIssuance.stakeholder_id, @@ -100,12 +109,28 @@ transactions.post("/issuance/stock", async (req, res) => { stock_legend_ids_mapping: incomingStockIssuance.stock_legend_ids_mapping, custom_id: incomingStockIssuance.custom_id || "", id: incomingStockIssuance.id, + + chain_id: issuer.chain_id, + stockClassContractId: stockClass.contract_id, + issuerPartyId: issuer.party_id, + stakeholderPartyId: stakeholder.party_id, }); // Update the stock issuance with tx_hash - await StockIssuance.findByIdAndUpdate(stockIssuance._id, { tx_hash: receipt.hash }); + await StockIssuance.findByIdAndUpdate(stockIssuance._id, { tx_hash }); + + // TODO save stakeholderStockPositionContractId + if (stakeholderStockPositionContractId) { + await Stakeholder.findByIdAndUpdate(stakeholder._id, { stock_position_contract_id: stakeholderStockPositionContractId }); + console.log("✅ | Stakeholder updated offchain with new Stock Position Contract ID", stakeholderStockPositionContractId); + } + + if (updatedStockClassContractId) { + await StockClass.findByIdAndUpdate(stockClass._id, { contract_id: updatedStockClassContractId }); + console.log("✅ | Stock Class updated offchain with new Contract ID", updatedStockClassContractId); + } - res.status(200).send({ stockIssuance: { ...stockIssuance.toObject(), tx_hash: receipt.hash } }); + res.status(200).send({ stockIssuance: { ...stockIssuance.toObject(), tx_hash } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 63b32d80721d0ed1377c66e71a5b9a1605fe0897 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 11 Jun 2025 12:37:35 -0400 Subject: [PATCH 16/25] Transfer --- .../canton/transferControllerCanton.js | 21 ++++++++++++++++++ .../transactions/transferController.js | 6 +++++ src/routes/transactions/base.js | 22 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/chain-operations/canton/transferControllerCanton.js diff --git a/src/chain-operations/canton/transferControllerCanton.js b/src/chain-operations/canton/transferControllerCanton.js new file mode 100644 index 00000000..3a9d73d4 --- /dev/null +++ b/src/chain-operations/canton/transferControllerCanton.js @@ -0,0 +1,21 @@ +import { client } from "./clientConfig"; + +export const convertAndCreateTransferStockOnchainCanton = async (contract, transfer) => { + const { transferorPartyId, transferorStockPositionContractId, transfereePartyId, quantity } = transfer; + + // Transferer proposes share transfer to transferee + const { transferProposalContractId, updatedStockPositionContractId } = await client.proposeTransfer( + transferorStockPositionContractId, + transfereePartyId, + quantity, + transferorPartyId + ); + + // Transferee accepts the transfer proposal and receives shares + const transfereeStockPositionContractId = await client.acceptTransfer(transferProposalContractId, transfereePartyId); + + return { + transferorUpdatedStockPositionContractId: updatedStockPositionContractId, + transfereeStockPositionContractId: transfereeStockPositionContractId, + }; +}; diff --git a/src/controllers/transactions/transferController.js b/src/controllers/transactions/transferController.js index 99d56305..18c6a450 100644 --- a/src/controllers/transactions/transferController.js +++ b/src/controllers/transactions/transferController.js @@ -1,8 +1,14 @@ import { convertUUIDToBytes16 } from "../../utils/convertUUID.js"; import { toScaledBigNumber } from "../../utils/convertToFixedPointDecimals.js"; import { decodeError } from "../../utils/errorDecoder.js"; +import { isCantonChainId } from "../../chain-operations/canton/constants.js"; +import { convertAndCreateTransferStockOnchainCanton } from "../../chain-operations/canton/transferControllerCanton.js"; export const convertAndCreateTransferStockOnchain = async (contract, transfer) => { + if (isCantonChainId(transfer.chain_id)) { + return convertAndCreateTransferStockOnchainCanton(contract, transfer); + } + try { const { quantity, transferorId, transfereeId, stockClassId, sharePrice } = transfer; diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index 44bde74b..1abdad03 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -142,10 +142,28 @@ transactions.post("/transfer/stock", async (req, res) => { const { issuerId, data } = req.body; try { - await readIssuerById(issuerId); + const issuer = await readIssuerById(issuerId); + const transferor = await readStakeholderById(data.transferorId); + const transferee = await readStakeholderById(data.transfereeId); // @dev: Transfer Validation is not possible through schema because it validates that the transfer has occurred,at this stage it has not yet. - await convertAndCreateTransferStockOnchain(contract, data); + const { transferorUpdatedStockPositionContractId, transfereeStockPositionContractId } = await convertAndCreateTransferStockOnchain(contract, { + ...data, + chain_id: issuer.chain_id, + transferorPartyId: transferor.party_id, + transferorStockPositionContractId: transferor.stock_position_contract_id, + transfereePartyId: transferee.party_id, + }); + + if (transferorUpdatedStockPositionContractId) { + await Stakeholder.findByIdAndUpdate(transferor._id, { stock_position_contract_id: transferorUpdatedStockPositionContractId }); + console.log("✅ | Transferor updated offchain with new Stock Position Contract ID", transferorUpdatedStockPositionContractId); + } + + if (transfereeStockPositionContractId) { + await Stakeholder.findByIdAndUpdate(transferee._id, { stock_position_contract_id: transfereeStockPositionContractId }); + console.log("✅ | Transferee updated offchain with new Stock Position Contract ID", transfereeStockPositionContractId); + } res.status(200).send("success"); } catch (error) { From 785a636dc126ded68f14de4616d21868091fdfc5 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 11:34:39 -0400 Subject: [PATCH 17/25] Bump lib --- src/chain-operations/canton/clientConfig.js | 2 +- src/chain-operations/canton/lib/fairmint-canton | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chain-operations/canton/clientConfig.js b/src/chain-operations/canton/clientConfig.js index e2454373..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(); +const config = new TransferAgentConfig(false); const client = new FairmintClient(config); export { config, client }; diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton index 0861261d..cd6877f4 160000 --- a/src/chain-operations/canton/lib/fairmint-canton +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -1 +1 @@ -Subproject commit 0861261d16ba7a70875e07ae5178985f90e217ae +Subproject commit cd6877f41bcf0731b775972c0c4d15afaf0619ec From 29b23fa9dd87bd33fa2850adb6045314e526f52c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 11:49:07 -0400 Subject: [PATCH 18/25] Bug fixes --- src/chain-operations/canton/stakeholderControllerCanton.js | 2 +- src/routes/stakeholder/base.js | 2 +- src/routes/stockClass.js | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/chain-operations/canton/stakeholderControllerCanton.js b/src/chain-operations/canton/stakeholderControllerCanton.js index e04738cf..57c13893 100644 --- a/src/chain-operations/canton/stakeholderControllerCanton.js +++ b/src/chain-operations/canton/stakeholderControllerCanton.js @@ -7,5 +7,5 @@ export const convertAndReflectStakeholderOnchainCanton = async (stakeholderId) = // Create new party for stakeholder [Once per stakeholder] const { partyId } = await client.createParty(stakeholderId); - return { partyId }; + return { partyId, updateId: null /* TODO */ }; }; diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index 44583699..3d58b06c 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -112,7 +112,7 @@ router.post("/create", async (req, res) => { // Save onchain let tx_hash; - let partyId; + let partyId = null; if (!isCantonChainId(issuer.chain_id)) { ({ hash: tx_hash } = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id)); await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash }); diff --git a/src/routes/stockClass.js b/src/routes/stockClass.js index 179d5b0d..45ce1d2a 100644 --- a/src/routes/stockClass.js +++ b/src/routes/stockClass.js @@ -46,6 +46,9 @@ stockClass.post("/create", async (req, res) => { try { const issuer = await readIssuerById(issuerId); + if (!issuer) { + return res.status(404).send({ message: "Issuer not found" }); + } // OCF doesn't allow extra fields in their validation const incomingStockClassToValidate = { @@ -74,7 +77,7 @@ stockClass.post("/create", async (req, res) => { stockClassContractId, updatedIssuerContractId, } = await convertAndReflectStockClassOnchain(contract, incomingStockClassForDB, issuer); - await Stockclass.findByIdAndUpdate(stockClass._id, { tx_hash, contract_id: stockClassContractId }); + await Stockclass.findByIdAndUpdate(stockClass._id, { tx_hash, contract_id: stockClassContractId ?? null }); console.log("✅ | Stock Class created offchain:", stockClass); if (updatedIssuerContractId) { @@ -82,7 +85,7 @@ stockClass.post("/create", async (req, res) => { console.log("✅ | Issuer updated offchain:", issuer); } - res.status(200).send({ stockClass: { ...stockClass.toObject(), tx_hash } }); + res.status(200).send({ stockClass: { ...stockClass.toObject(), tx_hash: tx_hash ?? null } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 40b1fcabb413ef72dccb43454df88fd48f81ee43 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 11:55:01 -0400 Subject: [PATCH 19/25] Fix --- src/routes/stakeholder/base.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/routes/stakeholder/base.js b/src/routes/stakeholder/base.js index 3d58b06c..e4fff84c 100644 --- a/src/routes/stakeholder/base.js +++ b/src/routes/stakeholder/base.js @@ -110,6 +110,10 @@ router.post("/create", async (req, res) => { }); } + // Save offchain + const stakeholder = await createStakeholder({ ...incomingStakeholderForDB }); + console.log("✅ | Stakeholder created offchain:", stakeholder); + // Save onchain let tx_hash; let partyId = null; @@ -120,12 +124,12 @@ router.post("/create", async (req, res) => { ({ updateId: tx_hash, partyId } = await convertAndReflectStakeholderOnchainCanton(incomingStakeholderForDB.id)); } - // Save offchain - const stakeholder = await createStakeholder({ ...incomingStakeholderForDB, party_id: partyId }); - - console.log("✅ | Stakeholder created offchain:", stakeholder); + if (partyId) { + await Stakeholder.findByIdAndUpdate(stakeholder._id, { party_id: partyId }); + console.log("✅ | Stakeholder updated offchain with new partyId:", partyId); + } - res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash } }); + res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash, partyId } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 53fd5894de90981a49c447429e569dd6125bc27b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 12:34:57 -0400 Subject: [PATCH 20/25] Fix --- src/routes/transactions/base.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index 1abdad03..ab2300f2 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -117,14 +117,13 @@ transactions.post("/issuance/stock", async (req, res) => { }); // Update the stock issuance with tx_hash - await StockIssuance.findByIdAndUpdate(stockIssuance._id, { tx_hash }); + await StockIssuance.findByIdAndUpdate(stockIssuance._id, { tx_hash: tx_hash ?? null }); - // TODO save stakeholderStockPositionContractId + // Canton only updates: if (stakeholderStockPositionContractId) { await Stakeholder.findByIdAndUpdate(stakeholder._id, { stock_position_contract_id: stakeholderStockPositionContractId }); console.log("✅ | Stakeholder updated offchain with new Stock Position Contract ID", stakeholderStockPositionContractId); } - if (updatedStockClassContractId) { await StockClass.findByIdAndUpdate(stockClass._id, { contract_id: updatedStockClassContractId }); console.log("✅ | Stock Class updated offchain with new Contract ID", updatedStockClassContractId); From 2435acde1cecccc2ce010cacd4e05fb49d901aab Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 12:44:23 -0400 Subject: [PATCH 21/25] Error checking --- src/routes/transactions/base.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index ab2300f2..f4159a42 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -143,7 +143,13 @@ transactions.post("/transfer/stock", async (req, res) => { try { const issuer = await readIssuerById(issuerId); const transferor = await readStakeholderById(data.transferorId); + if (!transferor) { + return res.status(404).send({ message: "Transferor not found" }); + } const transferee = await readStakeholderById(data.transfereeId); + if (!transferee) { + return res.status(404).send({ message: "Transferee not found" }); + } // @dev: Transfer Validation is not possible through schema because it validates that the transfer has occurred,at this stage it has not yet. const { transferorUpdatedStockPositionContractId, transfereeStockPositionContractId } = await convertAndCreateTransferStockOnchain(contract, { @@ -154,11 +160,11 @@ transactions.post("/transfer/stock", async (req, res) => { transfereePartyId: transferee.party_id, }); + // Canton only updates: if (transferorUpdatedStockPositionContractId) { await Stakeholder.findByIdAndUpdate(transferor._id, { stock_position_contract_id: transferorUpdatedStockPositionContractId }); console.log("✅ | Transferor updated offchain with new Stock Position Contract ID", transferorUpdatedStockPositionContractId); } - if (transfereeStockPositionContractId) { await Stakeholder.findByIdAndUpdate(transferee._id, { stock_position_contract_id: transfereeStockPositionContractId }); console.log("✅ | Transferee updated offchain with new Stock Position Contract ID", transfereeStockPositionContractId); From 492cc1969e9a8d5777dfcc61ea0bd0a85c726430 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 12:54:04 -0400 Subject: [PATCH 22/25] Add error checks --- src/routes/transactions/base.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index f4159a42..92804766 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -94,7 +94,13 @@ transactions.post("/issuance/stock", async (req, res) => { const stockIssuance = await createStockIssuance({ ...incomingStockIssuance, issuer: issuerId }); const stockClass = await readStockClassById(incomingStockIssuance.stock_class_id); + if (!stockClass) { + return res.status(404).send({ message: "Stock class not found" }); + } const stakeholder = await readStakeholderById(incomingStockIssuance.stakeholder_id); + if (!stakeholder) { + return res.status(404).send({ message: "Stakeholder not found" }); + } const { hash: tx_hash, From f17fa68bbff69a8496213846fbc461629fc7d987 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 13:50:26 -0400 Subject: [PATCH 23/25] Bug fix --- src/routes/transactions/base.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/transactions/base.js b/src/routes/transactions/base.js index 92804766..99fcce65 100644 --- a/src/routes/transactions/base.js +++ b/src/routes/transactions/base.js @@ -94,11 +94,11 @@ transactions.post("/issuance/stock", async (req, res) => { const stockIssuance = await createStockIssuance({ ...incomingStockIssuance, issuer: issuerId }); const stockClass = await readStockClassById(incomingStockIssuance.stock_class_id); - if (!stockClass) { + if (!stockClass?._id) { return res.status(404).send({ message: "Stock class not found" }); } const stakeholder = await readStakeholderById(incomingStockIssuance.stakeholder_id); - if (!stakeholder) { + if (!stakeholder?._id) { return res.status(404).send({ message: "Stakeholder not found" }); } @@ -135,7 +135,7 @@ transactions.post("/issuance/stock", async (req, res) => { console.log("✅ | Stock Class updated offchain with new Contract ID", updatedStockClassContractId); } - res.status(200).send({ stockIssuance: { ...stockIssuance.toObject(), tx_hash } }); + res.status(200).send({ stockIssuance: { ...stockIssuance.toObject(), tx_hash: tx_hash ?? null } }); } catch (error) { console.error(error); res.status(500).send(`${error}`); From 2db33980124111131e429f672414f1f606764b46 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 17:29:14 -0400 Subject: [PATCH 24/25] Bump for mainnet --- src/chain-operations/canton/lib/fairmint-canton | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain-operations/canton/lib/fairmint-canton b/src/chain-operations/canton/lib/fairmint-canton index cd6877f4..9be91106 160000 --- a/src/chain-operations/canton/lib/fairmint-canton +++ b/src/chain-operations/canton/lib/fairmint-canton @@ -1 +1 @@ -Subproject commit cd6877f41bcf0731b775972c0c4d15afaf0619ec +Subproject commit 9be91106195c491b5bec8e1c331c37e70a18213a From aa4597c31591a2ba4b47c5c4835cf6dfbe5c8b97 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 12 Jun 2025 17:30:11 -0400 Subject: [PATCH 25/25] Bump to mainnet --- 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 bb0ff1fa..62299357 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(false); +const config = new TransferAgentConfig(true); const client = new FairmintClient(config); export { config, client };