Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Comment thread
HardlyDifficult marked this conversation as resolved.
}
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
22 changes: 17 additions & 5 deletions src/routes/stakeholder/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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";
import { convertAndReflectStakeholderOnchainCanton } from "../../chain-operations/canton/stakeholderControllerCanton.js";

const router = Router();

Expand Down Expand Up @@ -109,15 +111,25 @@ router.post("/create", async (req, res) => {
}

// Save offchain
const stakeholder = await createStakeholder(incomingStakeholderForDB);
const stakeholder = await createStakeholder({ ...incomingStakeholderForDB });
console.log("✅ | Stakeholder created offchain:", stakeholder);

// Save onchain
const receipt = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id);
await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash: receipt.hash });
let tx_hash;
let partyId = null;
if (!isCantonChainId(issuer.chain_id)) {
({ hash: tx_hash } = await convertAndReflectStakeholderOnchain(contract, incomingStakeholderForDB.id));
await Stakeholder.findByIdAndUpdate(stakeholder._id, { tx_hash });
} else {
({ updateId: tx_hash, partyId } = await convertAndReflectStakeholderOnchainCanton(incomingStakeholderForDB.id));
}

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: receipt.hash } });
res.status(200).send({ stakeholder: { ...stakeholder.toObject(), tx_hash, partyId } });
} catch (error) {
console.error(error);
res.status(500).send(`${error}`);
Expand Down
Loading