Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ CONVERTIBLES_FACET=
EQUITY_COMPENSATION_FACET=
STOCK_PLAN_FACET=
WARRANT_FACET=
STAKEHOLDER_NFT_FACET=
STAKEHOLDER_NFT_FACET=

# Canton config
TRANSFER_AGENT_CLIENT_SECRET=
FAIRMINT_PARTY_ID=
FAIRMINT_USER_ID=
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/node_modules
/chain/out
.history
src/chain-operations/canton/lib

# Build output
/dist
Expand All @@ -14,4 +15,4 @@
.env*

# Logs
*.log
*.log
4 changes: 2 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -39,7 +39,7 @@ export default [
},
{
files: ["**/*.ts"],
ignores: ["node_modules/**"],
ignores: ["node_modules/**", "src/chain-operations/canton/lib/**"],
languageOptions: {
parser: tsparser,
parserOptions: {
Expand Down
7 changes: 7 additions & 0 deletions src/chain-operations/canton/clientConfig.js
Original file line number Diff line number Diff line change
@@ -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(true);
const client = new FairmintClient(config);

export { config, client };
9 changes: 9 additions & 0 deletions src/chain-operations/canton/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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;

export function isCantonChainId(chainId) {
return chainId == CANTON_MAINNET_CHAIN_ID || chainId == CANTON_DEVNET_CHAIN_ID;
}
29 changes: 29 additions & 0 deletions src/chain-operations/canton/deployCapTableCanton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { client } from "./clientConfig";

// eslint-disable-next-line no-unused-vars
export async function deployCapTableCanton(issuerId, initial_shares_authorized, chainId, issuer) {
console.log("🗽 | Deploying cap table on Canton...");

// 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 {
partyId: issuerPartyId,
// deployId: updateId, // TODO
address: issuerContractId,
};
}
1 change: 1 addition & 0 deletions src/chain-operations/canton/lib/fairmint-canton
Submodule fairmint-canton added at 9be911
11 changes: 11 additions & 0 deletions src/chain-operations/canton/stakeholderControllerCanton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { client } from "./clientConfig";

// eslint-disable-next-line no-unused-vars
export const convertAndReflectStakeholderOnchainCanton = async (stakeholderId) => {
console.log("🗽 | Converting and reflecting stakeholder onchain Canton...");

// Create new party for stakeholder [Once per stakeholder]
const { partyId } = await client.createParty(stakeholderId);

return { partyId, updateId: null /* TODO */ };
};
13 changes: 13 additions & 0 deletions src/chain-operations/canton/stockClassControllerCanton.js
Original file line number Diff line number Diff line change
@@ -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 };
};
21 changes: 21 additions & 0 deletions src/chain-operations/canton/transferControllerCanton.js
Original file line number Diff line number Diff line change
@@ -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,
};
};
8 changes: 7 additions & 1 deletion src/chain-operations/deployCapTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isCantonChainId } from "./canton/constants.js";
import { deployCapTableCanton } from "./canton/deployCapTableCanton.js";

setupEnv();

Expand All @@ -20,7 +22,11 @@ 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, issuer);
}

// Get provider for specified chain
const wallet = await getWallet(chainId);
console.log("🗽 | Wallet address: ", wallet.address);
Expand Down
6 changes: 6 additions & 0 deletions src/chain-operations/getContractInstances.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
5 changes: 5 additions & 0 deletions src/chain-operations/getProvider.js
Original file line number Diff line number Diff line change
@@ -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}`);
Expand Down
19 changes: 19 additions & 0 deletions src/chain-operations/issuanceControllerCanton.js
Original file line number Diff line number Diff line change
@@ -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,
};
};
8 changes: 7 additions & 1 deletion src/controllers/stockClassController.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
20 changes: 19 additions & 1 deletion src/controllers/transactions/issuanceController.js
Original file line number Diff line number Diff line change
@@ -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),
Expand Down
6 changes: 6 additions & 0 deletions src/controllers/transactions/transferController.js
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/db/objects/Issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
party_id: { type: String, default: null },
},
{ timestamps: true }
);
Expand Down
2 changes: 2 additions & 0 deletions src/db/objects/Stakeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const StakeholderSchema = new mongoose.Schema(
addresses: [{}],
tax_ids: [{}],
tx_hash: { type: String, default: null },
party_id: { type: String, default: null },
stock_position_contract_id: { type: String, default: null },
},
{ timestamps: true }
);
Expand Down
1 change: 1 addition & 0 deletions src/db/objects/StockClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
);
Expand Down
3 changes: 2 additions & 1 deletion src/examples/testTransfer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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_DEVNET_CHAIN_ID } from "../chain-operations/canton/constants.js";

const main = async () => {
try {
Expand All @@ -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_DEVNET_CHAIN_ID;
const issuerResponse = await axios.post("http://localhost:8080/issuer/create", issuer);
console.log("✅ Issuer created:", issuerResponse.data);

Expand Down
8 changes: 7 additions & 1 deletion src/routes/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,19 @@ 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, partyId } = await deployCapTable(
issuerIdBytes16,
incomingIssuerToValidate.initial_shares_authorized,
chain_id,
incomingIssuerToValidate
);

const incomingIssuerForDB = {
...incomingIssuerToValidate,
deployed_to: address,
tx_hash: deployHash,
chain_id,
party_id: partyId,
};

const issuer = await createIssuer(incomingIssuerForDB);
Expand Down
35 changes: 35 additions & 0 deletions src/routes/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,39 @@ router.post("/verify", async (req, res) => {
}
});

router.post("/cross-id-check", async (req, res) => {
try {
// Validate request body
if (!req.body || Object.keys(req.body).length === 0) {
return res.status(400).send({
error: "Request body is empty or invalid JSON.",
valid: false,
});
}

// Get data from workspace JSON
const data = await getAllStateMachineObjectsFromWorkspaceJSON(req.body);

// Validate cap table data
const validationErrors = await validateCapTableData(data);
if (validationErrors.length > 0) {
return res.status(400).send({
errors: validationErrors,
valid: false,
});
}

return res.status(200).send({
valid: true,
errors: [],
});
} catch (error) {
console.error("Cap table verification error:", error);
return res.status(500).send({
error: String(error),
valid: false,
});
}
});

export default router;
Loading
Loading