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
225 changes: 225 additions & 0 deletions src/commands/title-escrow/accept-returned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { input } from '@inquirer/prompts';
import { error, success, info } from 'signale';
import signale from 'signale';
import { TransactionReceipt } from 'ethers';
import { v5Contracts, CHAIN_ID, acceptReturned as acceptReturnedImpl } from '@trustvc/trustvc';
import { BaseTitleEscrowCommand as TitleEscrowReturnDocumentCommand } from '../../types';
import {
displayTransactionPrice,
getErrorMessage,
getEtherscanAddress,
NetworkCmdName,
promptRemarkAndEncryptionKey,
promptNetworkSelection,
promptWalletSelection,
TransactionReceiptFees,
getSupportedNetwork,
getWalletOrSigner,
dryRunMode,
canEstimateGasPrice,
getGasFees,
} from '../../utils';
import { validateAndEncryptRemark } from '../helpers';

const { TradeTrustToken__factory } = v5Contracts;

export const command = 'accept-returned';

export const describe = 'Accepts a returned transferable record on the blockchain';

export const handler = async (): Promise<string | undefined> => {
try {
const answers = await promptForInputs();
if (!answers) return;

await acceptReturnedDocumentHandler(answers);
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};

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

// Token Registry Address
const tokenRegistry = 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;
},
});

// Token ID (Document Hash)
const tokenId = await input({
message: 'Enter the document hash (tokenId) that was returned:',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Token ID is required';
}
return true;
},
});

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

// Optional: Remark and Encryption Key
const { remark, encryptionKey } = await promptRemarkAndEncryptionKey();

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

// Add wallet-specific properties based on selected wallet type
if (encryptedWalletPath) {
return {
...baseResult,
encryptedWalletPath,
} as TitleEscrowReturnDocumentCommand;
} else if (keyFile) {
return {
...baseResult,
keyFile,
} as TitleEscrowReturnDocumentCommand;
} else if (key) {
return {
...baseResult,
key,
} as TitleEscrowReturnDocumentCommand;
}

// For environment variable case (when all wallet options are undefined)
return baseResult as TitleEscrowReturnDocumentCommand;
};

// Accept the returned document with the provided inputs
export const acceptReturnedDocumentHandler = async (args: TitleEscrowReturnDocumentCommand) => {
try {
info(`Accepting returned document with hash ${args.tokenId}`);

const transaction = await acceptReturned(args);

const network = args.network as NetworkCmdName;
displayTransactionPrice(transaction as unknown as TransactionReceiptFees, network);
const { hash: transactionHash } = transaction;

success(`Returned transferable record with hash ${args.tokenId} has been accepted.`);
info(
`Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transactionHash}`,
);

return args.tokenRegistryAddress;
} catch (e) {
error(getErrorMessage(e));
}
};

/**
* Accepts a returned transferable record (title escrow document) and burns the token.
* This operation is performed by the issuer after a document has been returned to them.
*
* @param tokenRegistryAddress - The address of the token registry contract
* @param tokenId - The unique identifier of the token to accept and burn
* @param remark - Optional remark/comment to attach to the transaction
* @param encryptionKey - Optional encryption key for encrypting the remark
* @param network - The blockchain network to execute the transaction on
* @param dryRun - If true, simulates the transaction without executing it
* @param rest - Additional parameters (e.g., wallet configuration, gas settings)
* @returns Promise resolving to the transaction receipt
* @throws Error if provider is required but not available, or if transaction receipt is null
*/
export const acceptReturned = async ({
tokenRegistryAddress,
tokenId,
remark,
encryptionKey,
network,
dryRun,
...rest
}: TitleEscrowReturnDocumentCommand): Promise<TransactionReceipt> => {
// Initialize wallet/signer for the transaction
const wallet = await getWalletOrSigner({ network, ...rest });

// Get the network ID for the specified network
const networkId = getSupportedNetwork(network).networkId;

// Validate and encrypt the remark if encryption key is provided
const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey);
// Connect to the token registry contract instance
const tokenRegistryInstance = await TradeTrustToken__factory.connect(
tokenRegistryAddress,
wallet,
);
// Dry run mode: estimate gas and exit without executing the transaction
if (dryRun) {
await dryRunMode({
estimatedGas: await tokenRegistryInstance.estimateGas.burn(tokenId, encryptedRemark),
network,
});
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 });

// Execute accept returned with EIP-1559 gas parameters
transaction = await acceptReturnedImpl(
{ tokenRegistryAddress },
wallet,
{ tokenId, remarks: remark },
{
chainId: networkId as unknown as CHAIN_ID,
maxFeePerGas: gasFees.maxFeePerGas?.toString(),
maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(),
id: encryptionKey,
},
);
} else {
// Execute accept returned without gas estimation (for networks that don't support it)
transaction = await acceptReturnedImpl(
{ tokenRegistryAddress },
wallet,
{ tokenId, remarks: remark },
{
chainId: networkId as unknown as CHAIN_ID,
id: encryptionKey,
},
);
}

// Wait for transaction to be mined
signale.await(`Waiting for transaction ${transaction.hash} to be mined`);
const receipt = await transaction.wait();

// Validate receipt exists
if (!receipt) {
throw new Error('Transaction receipt is null');
}

return receipt as unknown as TransactionReceipt;
};
Loading