Skip to content

Commit 7b45aa5

Browse files
committed
feat: return commands
1 parent f50ce1b commit 7b45aa5

12 files changed

Lines changed: 2439 additions & 0 deletions
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { input } from '@inquirer/prompts';
2+
import { error, success, info } from 'signale';
3+
import signale from 'signale';
4+
import { TransactionReceipt } from 'ethers';
5+
import { v5Contracts, CHAIN_ID, acceptReturned as acceptReturnedImpl } from '@trustvc/trustvc';
6+
import { BaseTitleEscrowCommand as TitleEscrowReturnDocumentCommand } from '../../types';
7+
import {
8+
displayTransactionPrice,
9+
getErrorMessage,
10+
getEtherscanAddress,
11+
NetworkCmdName,
12+
promptRemarkAndEncryptionKey,
13+
promptNetworkSelection,
14+
promptWalletSelection,
15+
TransactionReceiptFees,
16+
getSupportedNetwork,
17+
getWalletOrSigner,
18+
dryRunMode,
19+
canEstimateGasPrice,
20+
getGasFees,
21+
} from '../../utils';
22+
import { validateAndEncryptRemark } from '../helpers';
23+
24+
const { TradeTrustToken__factory } = v5Contracts;
25+
26+
export const command = 'accept-returned';
27+
28+
export const describe = 'Accepts a returned transferable record on the blockchain';
29+
30+
export const handler = async (): Promise<string | undefined> => {
31+
try {
32+
const answers = await promptForInputs();
33+
if (!answers) return;
34+
35+
await acceptReturnedDocumentHandler(answers);
36+
} catch (err: unknown) {
37+
error(err instanceof Error ? err.message : String(err));
38+
}
39+
};
40+
41+
// Prompt user for all required inputs
42+
export const promptForInputs = async (): Promise<TitleEscrowReturnDocumentCommand> => {
43+
// Network selection
44+
const network = await promptNetworkSelection();
45+
46+
// Token Registry Address
47+
const tokenRegistry = await input({
48+
message: 'Enter the token registry contract address:',
49+
required: true,
50+
validate: (value: string) => {
51+
if (!value || value.trim() === '') {
52+
return 'Token registry address is required';
53+
}
54+
if (!/^0x[a-fA-F0-9]{40}$/.test(value)) {
55+
return 'Invalid Ethereum address format';
56+
}
57+
return true;
58+
},
59+
});
60+
61+
// Token ID (Document Hash)
62+
const tokenId = await input({
63+
message: 'Enter the document hash (tokenId) that was returned:',
64+
required: true,
65+
validate: (value: string) => {
66+
if (!value || value.trim() === '') {
67+
return 'Token ID is required';
68+
}
69+
return true;
70+
},
71+
});
72+
73+
// Wallet selection
74+
const { encryptedWalletPath, key, keyFile } = await promptWalletSelection();
75+
76+
// Optional: Remark and Encryption Key
77+
const { remark, encryptionKey } = await promptRemarkAndEncryptionKey();
78+
79+
// Build the result object with proper typing
80+
const baseResult = {
81+
network,
82+
tokenRegistryAddress: tokenRegistry,
83+
tokenId,
84+
remark,
85+
encryptionKey,
86+
dryRun: false,
87+
maxPriorityFeePerGasScale: 1,
88+
};
89+
90+
// Add wallet-specific properties based on selected wallet type
91+
if (encryptedWalletPath) {
92+
return {
93+
...baseResult,
94+
encryptedWalletPath,
95+
} as TitleEscrowReturnDocumentCommand;
96+
} else if (keyFile) {
97+
return {
98+
...baseResult,
99+
keyFile,
100+
} as TitleEscrowReturnDocumentCommand;
101+
} else if (key) {
102+
return {
103+
...baseResult,
104+
key,
105+
} as TitleEscrowReturnDocumentCommand;
106+
}
107+
108+
// For environment variable case (when all wallet options are undefined)
109+
return baseResult as TitleEscrowReturnDocumentCommand;
110+
};
111+
112+
// Accept the returned document with the provided inputs
113+
export const acceptReturnedDocumentHandler = async (args: TitleEscrowReturnDocumentCommand) => {
114+
try {
115+
info(`Accepting returned document with hash ${args.tokenId}`);
116+
117+
const transaction = await acceptReturned(args);
118+
119+
const network = args.network as NetworkCmdName;
120+
displayTransactionPrice(transaction as unknown as TransactionReceiptFees, network);
121+
const { hash: transactionHash } = transaction;
122+
123+
success(`Returned transferable record with hash ${args.tokenId} has been accepted.`);
124+
info(
125+
`Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transactionHash}`,
126+
);
127+
128+
return args.tokenRegistryAddress;
129+
} catch (e) {
130+
error(getErrorMessage(e));
131+
}
132+
};
133+
134+
/**
135+
* Accepts a returned transferable record (title escrow document) and burns the token.
136+
* This operation is performed by the issuer after a document has been returned to them.
137+
*
138+
* @param tokenRegistryAddress - The address of the token registry contract
139+
* @param tokenId - The unique identifier of the token to accept and burn
140+
* @param remark - Optional remark/comment to attach to the transaction
141+
* @param encryptionKey - Optional encryption key for encrypting the remark
142+
* @param network - The blockchain network to execute the transaction on
143+
* @param dryRun - If true, simulates the transaction without executing it
144+
* @param rest - Additional parameters (e.g., wallet configuration, gas settings)
145+
* @returns Promise resolving to the transaction receipt
146+
* @throws Error if provider is required but not available, or if transaction receipt is null
147+
*/
148+
export const acceptReturned = async ({
149+
tokenRegistryAddress,
150+
tokenId,
151+
remark,
152+
encryptionKey,
153+
network,
154+
dryRun,
155+
...rest
156+
}: TitleEscrowReturnDocumentCommand): Promise<TransactionReceipt> => {
157+
// Initialize wallet/signer for the transaction
158+
const wallet = await getWalletOrSigner({ network, ...rest });
159+
160+
// Get the network ID for the specified network
161+
const networkId = getSupportedNetwork(network).networkId;
162+
163+
// Validate and encrypt the remark if encryption key is provided
164+
const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey);
165+
// Connect to the token registry contract instance
166+
const tokenRegistryInstance = await TradeTrustToken__factory.connect(
167+
tokenRegistryAddress,
168+
wallet,
169+
);
170+
// Dry run mode: estimate gas and exit without executing the transaction
171+
if (dryRun) {
172+
await dryRunMode({
173+
estimatedGas: await tokenRegistryInstance.estimateGas.burn(tokenId, encryptedRemark),
174+
network,
175+
});
176+
process.exit(0);
177+
}
178+
let transaction;
179+
180+
// Execute transaction with appropriate gas settings based on network capabilities
181+
if (canEstimateGasPrice(network)) {
182+
// Ensure provider is available for gas estimation
183+
if (!wallet.provider) {
184+
throw new Error('Provider is required for gas estimation');
185+
}
186+
187+
// Get current gas fees from the network
188+
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
189+
190+
// Execute accept returned with EIP-1559 gas parameters
191+
transaction = await acceptReturnedImpl(
192+
{ tokenRegistryAddress },
193+
wallet,
194+
{ tokenId, remarks: remark },
195+
{
196+
chainId: networkId as unknown as CHAIN_ID,
197+
maxFeePerGas: gasFees.maxFeePerGas?.toString(),
198+
maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(),
199+
id: encryptionKey,
200+
},
201+
);
202+
} else {
203+
// Execute accept returned without gas estimation (for networks that don't support it)
204+
transaction = await acceptReturnedImpl(
205+
{ tokenRegistryAddress },
206+
wallet,
207+
{ tokenId, remarks: remark },
208+
{
209+
chainId: networkId as unknown as CHAIN_ID,
210+
id: encryptionKey,
211+
},
212+
);
213+
}
214+
215+
// Wait for transaction to be mined
216+
signale.await(`Waiting for transaction ${transaction.hash} to be mined`);
217+
const receipt = await transaction.wait();
218+
219+
// Validate receipt exists
220+
if (!receipt) {
221+
throw new Error('Transaction receipt is null');
222+
}
223+
224+
return receipt as unknown as TransactionReceipt;
225+
};

0 commit comments

Comments
 (0)