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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ didKeyPairs.json
wellknown.json
credentialStatus.json
signed_vc.json
test.json
testSepolia.json

# Dependencies
node_modules/
Expand Down
85 changes: 83 additions & 2 deletions src/commands/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
// External dependencies
import { BytesLike, Wallet, HDNodeWallet, ZeroAddress } from 'ethers';
import { BytesLike, Wallet, HDNodeWallet, ZeroAddress, Provider } from 'ethers';
import signale from 'signale';
import { v5Contracts } from '@trustvc/trustvc';
import { v5Contracts, checkSupportsInterface, v4SupportInterfaceIds, v5SupportInterfaceIds } from '@trustvc/trustvc';
import { encrypt } from '@trustvc/trustvc';

// Internal utilities
import { ConnectedSigner } from '../utils';

// Contract factories from TrustVC v5
const { TitleEscrow__factory, TradeTrustToken__factory } = v5Contracts;

// Interface for connectToTokenRegistry function arguments
interface ConnectToTokenRegistryArgs {
address: string;
wallet: Wallet | HDNodeWallet | ConnectedSigner;
}

/**
* Connects to a token registry contract instance.
*
* @param address - The address of the token registry contract
* @param wallet - The wallet or signer to use for the connection
* @returns Promise resolving to the connected TradeTrustToken contract instance
* @throws Error if connection fails
*/
export const connectToTokenRegistry = async ({
address,
wallet,
}: ConnectToTokenRegistryArgs): Promise<InstanceType<typeof TradeTrustToken__factory>> => {
try {
// Connect to the token registry contract
signale.info(`Connecting to token registry at: ${address}`);
const tokenRegistry = TradeTrustToken__factory.connect(address, wallet);

// Validate the connection was successful
if (!tokenRegistry) {
const error = `Failed to connect to token registry at address: ${address}`;
signale.error(error);
throw new Error(error);
}

signale.success(`Successfully connected to token registry`);
return tokenRegistry;
} catch (error) {
signale.error(
`Error in connectToTokenRegistry: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
}
};
// Interface for connectToTitleEscrow function arguments
interface ConnectToTitleEscrowArgs {
tokenId: string;
Expand Down Expand Up @@ -232,3 +272,44 @@ export const validateAndEncryptRemark = (remark?: string, keyId?: string): Bytes
const encrpyted = encrypt(remark, keyId ?? '');
return encrpyted.startsWith('0x') ? encrpyted : `0x${encrpyted}`;
};

/**
* Determines the version of a token registry contract by checking supported interfaces.
* Checks for V5 first, then V4 compatibility.
*
* @param tokenRegistryAddress - The address of the token registry contract
* @param provider - The provider to use for the contract call
* @returns Promise resolving to 'v5', 'v4', or 'unknown'
*/
export const getTokenRegistryVersion = async (
tokenRegistryAddress: string,
provider: Provider,
): Promise<'v5' | 'v4' | 'unknown'> => {
try {
// Check if it's V5
const isV5 = await checkSupportsInterface(
tokenRegistryAddress,
v5SupportInterfaceIds.SBT,
provider,
);

if (isV5) {
return 'v5';
}

// Check if it's V4
const isV4 = await checkSupportsInterface(
tokenRegistryAddress,
v4SupportInterfaceIds.SBT,
provider,
);

if (isV4) {
return 'v4';
}

return 'unknown';
} catch (error) {
throw new Error(`Failed to determine token registry version: ${error instanceof Error ? error.message : String(error)}`);
}
};
150 changes: 74 additions & 76 deletions src/commands/token-registry/mint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { input } from '@inquirer/prompts';
import signale, { error, info, success } from 'signale';
import { TokenRegistryMintCommand } from '../../types';
import {
Expand All @@ -7,14 +6,18 @@ import {
getErrorMessage,
getEtherscanAddress,
NetworkCmdName,
promptRemarkAndEncryptionKey,
promptNetworkSelection,
promptWalletSelection,
getWalletOrSigner,
canEstimateGasPrice,
getGasFees,
extractDocumentInfo,
promptAndReadDocument,
promptRemark,
promptAddress,
performDryRunWithConfirmation,
} from '../../utils';
import { BigNumberish, TransactionReceipt } from 'ethers';
import { connectToTokenRegistry, validateAndEncryptRemark } from '../helpers';
import { TransactionReceipt } from 'ethers';
import { mint } from '@trustvc/trustvc';

export const command = 'mint';
Expand All @@ -34,82 +37,37 @@ export const handler = async (): Promise<void> => {

// Prompt user for all required inputs
export const promptForInputs = async (): Promise<TokenRegistryMintCommand> => {
// Network selection
const network = await promptNetworkSelection();

// Token Registry Address
const address = await input({
message: 'Enter the token registry contract address:',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Token registry address is required';
}
if (!/^0x[a-fA-F0-9]{40}$/.test(value)) {
return 'Invalid Ethereum address format';
}
return true;
},
});
// Extract document information using utility function
const document = await promptAndReadDocument();

// Token ID (Document Hash)
const tokenId = await input({
message: 'Enter the document hash (tokenId) to mint:',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Token ID is required';
}
return true;
},
});
// Extract document information using utility function
const { tokenRegistry, tokenId, network, documentId, registryVersion } =
await extractDocumentInfo(document);

// Beneficiary Address
const beneficiary = await input({
message: 'Enter the beneficiary address (initial recipient):',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Beneficiary address is required';
}
if (!/^0x[a-fA-F0-9]{40}$/.test(value)) {
return 'Invalid Ethereum address format';
}
return true;
},
});
const beneficiary = await promptAddress('beneficiary', 'initial recipient');

// Holder Address
const holder = await input({
message: 'Enter the holder address (initial holder):',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Holder address is required';
}
if (!/^0x[a-fA-F0-9]{40}$/.test(value)) {
return 'Invalid Ethereum address format';
}
return true;
},
});
const holder = await promptAddress('holder', 'initial holder');

// Wallet selection
const { encryptedWalletPath, key, keyFile } = await promptWalletSelection();

// Optional: Remark and Encryption Key
const { remark, encryptionKey } = await promptRemarkAndEncryptionKey();
// Optional: Remark (only for V5)
const remark = await promptRemark(registryVersion);

// Use document ID as encryption key
const encryptionKey = documentId;

// Build the result object with proper typing
const baseResult = {
network,
address,
address: tokenRegistry,
tokenId,
beneficiary,
holder,
remark,
encryptionKey,
dryRun: false,
maxPriorityFeePerGasScale: 1,
};

Expand Down Expand Up @@ -173,34 +131,74 @@ const mintToTokenRegistry = async ({
remark,
encryptionKey,
network,
dryRun,
...rest
}: TokenRegistryMintCommand): Promise<TransactionReceipt> => {
const wallet = await getWalletOrSigner({ network, ...rest });
let transactionOptions: { maxFeePerGas?: BigNumberish; maxPriorityFeePerGas?: BigNumberish } = {};

if (dryRun) {
console.log('🔧 Dry run mode is currently undergoing upgrades and will be available soon.');
// Automatic dry run for Ethereum and Polygon networks
const shouldProceed = await performDryRunWithConfirmation({
network,
getTransactionCallback: async () => {
const tokenRegistry = await connectToTokenRegistry({ address, wallet });

// Validate and encrypt the remark with document ID as encryption key
const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey);

// Populate the transaction for gas estimation
const tx = await tokenRegistry.mint.populateTransaction(
beneficiary,
holder,
tokenId,
encryptedRemark
);

// Ensure the transaction has a 'from' address for proper gas estimation
return {
...tx,
from: await wallet.getAddress(),
};
},
});

if (!shouldProceed) {
process.exit(0);
}

let transaction;

// Execute transaction with appropriate gas settings based on network capabilities
if (canEstimateGasPrice(network)) {
// Ensure provider is available for gas estimation
if (!wallet.provider) {
throw new Error('Provider is required for gas estimation');
}

// Get current gas fees from the network
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
transactionOptions = {
maxFeePerGas: gasFees.maxFeePerGas as BigNumberish,
maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas as BigNumberish,
};

// Execute mint with EIP-1559 gas parameters
transaction = await mint(
{ tokenRegistryAddress: address },
wallet,
{ beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark },
{
id: encryptionKey,
maxFeePerGas: gasFees.maxFeePerGas?.toString(),
maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(),
},
);
} else {
// Execute mint without gas estimation (for networks that don't support it)
transaction = await mint(
{ tokenRegistryAddress: address },
wallet,
{ beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark },
{
id: encryptionKey,
},
);
}

const transaction = await mint(
{ tokenRegistryAddress: address },
wallet,
{ beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark },
{ id: encryptionKey, ...transactionOptions },
);
signale.await(`Waiting for transaction ${transaction.hash} to be mined`);
const receipt = (await transaction.wait()) as unknown as TransactionReceipt;
if (!receipt) {
Expand Down
10 changes: 8 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { credentialStatus, issuer, RawVerifiableCredential } from '@trustvc/trustvc';
import { GasOption, NetworkOption, RpcUrlOption, WalletOrSignerOption } from './utils';
import {
GasOption,
NetworkAndWalletSignerOption,
NetworkOption,
RpcUrlOption,
WalletOrSignerOption,
} from './utils';

export type SignInput = {
credential: RawVerifiableCredential;
keyPairData: typeof issuer.IssuedDIDOption;
encryptionAlgorithm: typeof credentialStatus.cryptoSuiteName;
pathToSignedVC: string;
}
};
export type DidInput = {
keyPairPath: string;
domainName: string;
Expand Down
Loading