ethnotary 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ const { getContacts } = require('./contacts');
2
+
3
+ const DEFAULT_BASE_URL = 'https://ethnotary.io';
4
+
5
+ /**
6
+ * Generate approval URL for a transaction
7
+ * @param {number} txId - Transaction ID
8
+ * @param {string} network - Network name (e.g., 'sepolia')
9
+ * @param {string} contractAddress - MultiSig contract address
10
+ * @returns {string} Approval URL
11
+ */
12
+ function generateApprovalUrl(txId, network, contractAddress) {
13
+ const baseUrl = process.env.ETHNOTARY_BASE_URL || DEFAULT_BASE_URL;
14
+ return `${baseUrl}/app/views/txn.html?txid=${txId}&network=${network}&address=${contractAddress}`;
15
+ }
16
+
17
+ /**
18
+ * Format approval request message
19
+ * @param {Object} txData - Transaction data
20
+ * @returns {string} Formatted message
21
+ */
22
+ function formatApprovalMessage(txData) {
23
+ const lines = [
24
+ '🔔 MultiSig Approval Request',
25
+ '',
26
+ `Transaction #${txData.id} on ${txData.network}`,
27
+ `To: ${txData.destination}`,
28
+ ];
29
+
30
+ if (txData.value && txData.value !== '0 ETH') {
31
+ lines.push(`Value: ${txData.value}`);
32
+ }
33
+
34
+ if (txData.data && txData.data !== '0x') {
35
+ lines.push(`Data: ${txData.data.slice(0, 20)}...`);
36
+ }
37
+
38
+ lines.push('');
39
+ lines.push(`Confirmations: ${txData.confirmations}/${txData.required}`);
40
+ lines.push('');
41
+ lines.push(`👉 Review & Approve: ${txData.approvalUrl}`);
42
+
43
+ return lines.join('\n');
44
+ }
45
+
46
+ /**
47
+ * Get notification data for other owners (excluding sender)
48
+ * @param {string[]} owners - Array of owner addresses
49
+ * @param {string} senderAddress - Address of the transaction sender
50
+ * @param {Object} txData - Transaction data for message formatting
51
+ * @returns {Array} Array of notification objects for each owner with contact info
52
+ */
53
+ function getNotificationData(owners, senderAddress, txData) {
54
+ // Filter out the sender
55
+ const otherOwners = owners.filter(
56
+ addr => addr.toLowerCase() !== senderAddress.toLowerCase()
57
+ );
58
+
59
+ // Get contacts for other owners
60
+ const contacts = getContacts(otherOwners);
61
+
62
+ // Build notification data for each owner with contact info
63
+ const notifications = [];
64
+
65
+ for (const owner of otherOwners) {
66
+ const contact = contacts.find(
67
+ c => c.address.toLowerCase() === owner.toLowerCase()
68
+ );
69
+
70
+ if (contact && (contact.telegram || contact.whatsapp || contact.webhook)) {
71
+ notifications.push({
72
+ address: owner,
73
+ telegram: contact.telegram || null,
74
+ whatsapp: contact.whatsapp || null,
75
+ webhook: contact.webhook || null,
76
+ message: formatApprovalMessage(txData)
77
+ });
78
+ }
79
+ }
80
+
81
+ return notifications;
82
+ }
83
+
84
+ /**
85
+ * Build complete notification payload for a transaction
86
+ * @param {Object} params - Parameters
87
+ * @param {number} params.transactionId - Transaction ID
88
+ * @param {string} params.network - Network name
89
+ * @param {string} params.contractAddress - MultiSig address
90
+ * @param {string} params.destination - Transaction destination
91
+ * @param {string} params.value - Transaction value
92
+ * @param {string} params.data - Transaction data
93
+ * @param {number} params.confirmations - Current confirmation count
94
+ * @param {number} params.required - Required confirmations
95
+ * @param {string[]} params.owners - Array of owner addresses
96
+ * @param {string} params.senderAddress - Sender's address
97
+ * @returns {Object} Notification payload
98
+ */
99
+ function buildNotificationPayload(params) {
100
+ const approvalUrl = generateApprovalUrl(
101
+ params.transactionId,
102
+ params.network,
103
+ params.contractAddress
104
+ );
105
+
106
+ const txData = {
107
+ id: params.transactionId,
108
+ network: params.network,
109
+ destination: params.destination,
110
+ value: params.value,
111
+ data: params.data,
112
+ confirmations: params.confirmations,
113
+ required: params.required,
114
+ approvalUrl
115
+ };
116
+
117
+ const notifyOwners = getNotificationData(
118
+ params.owners,
119
+ params.senderAddress,
120
+ txData
121
+ );
122
+
123
+ return {
124
+ approvalUrl,
125
+ notifyOwners,
126
+ message: formatApprovalMessage(txData)
127
+ };
128
+ }
129
+
130
+ module.exports = {
131
+ generateApprovalUrl,
132
+ formatApprovalMessage,
133
+ getNotificationData,
134
+ buildNotificationPayload
135
+ };
@@ -0,0 +1,123 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+
4
+ class Output {
5
+ constructor(options = {}) {
6
+ this.jsonMode = options.json || false;
7
+ this.spinner = null;
8
+ }
9
+
10
+ // Output data - JSON if --json flag, formatted otherwise
11
+ print(data, options = {}) {
12
+ if (this.jsonMode) {
13
+ console.log(JSON.stringify(data, null, 2));
14
+ } else {
15
+ if (options.title) {
16
+ console.log(chalk.bold(options.title));
17
+ }
18
+ if (typeof data === 'string') {
19
+ console.log(data);
20
+ } else {
21
+ this.printFormatted(data);
22
+ }
23
+ }
24
+ }
25
+
26
+ printFormatted(data, indent = 0) {
27
+ const prefix = ' '.repeat(indent);
28
+ if (Array.isArray(data)) {
29
+ data.forEach((item, i) => {
30
+ if (typeof item === 'object') {
31
+ console.log(`${prefix}${chalk.dim(`[${i}]`)}`);
32
+ this.printFormatted(item, indent + 1);
33
+ } else {
34
+ console.log(`${prefix}- ${item}`);
35
+ }
36
+ });
37
+ } else if (typeof data === 'object' && data !== null) {
38
+ Object.entries(data).forEach(([key, value]) => {
39
+ if (typeof value === 'object' && value !== null) {
40
+ console.log(`${prefix}${chalk.cyan(key)}:`);
41
+ this.printFormatted(value, indent + 1);
42
+ } else {
43
+ console.log(`${prefix}${chalk.cyan(key)}: ${value}`);
44
+ }
45
+ });
46
+ } else {
47
+ console.log(`${prefix}${data}`);
48
+ }
49
+ }
50
+
51
+ success(message) {
52
+ if (!this.jsonMode) {
53
+ console.log(chalk.green('✓'), message);
54
+ }
55
+ }
56
+
57
+ error(message, exitCode = 1) {
58
+ if (this.jsonMode) {
59
+ console.log(JSON.stringify({ error: message }));
60
+ } else {
61
+ console.error(chalk.red('✗'), message);
62
+ }
63
+ process.exit(exitCode);
64
+ }
65
+
66
+ warn(message) {
67
+ if (!this.jsonMode) {
68
+ console.log(chalk.yellow('âš '), message);
69
+ }
70
+ }
71
+
72
+ info(message) {
73
+ if (!this.jsonMode) {
74
+ console.log(chalk.blue('ℹ'), message);
75
+ }
76
+ }
77
+
78
+ // Spinner - no-op in JSON mode
79
+ startSpinner(text) {
80
+ if (!this.jsonMode) {
81
+ this.spinner = ora(text).start();
82
+ }
83
+ }
84
+
85
+ updateSpinner(text) {
86
+ if (this.spinner) {
87
+ this.spinner.text = text;
88
+ }
89
+ }
90
+
91
+ succeedSpinner(text) {
92
+ if (this.spinner) {
93
+ this.spinner.succeed(text);
94
+ this.spinner = null;
95
+ }
96
+ }
97
+
98
+ failSpinner(text) {
99
+ if (this.spinner) {
100
+ this.spinner.fail(text);
101
+ this.spinner = null;
102
+ }
103
+ }
104
+
105
+ stopSpinner() {
106
+ if (this.spinner) {
107
+ this.spinner.stop();
108
+ this.spinner = null;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Create output instance from command options
114
+ function createOutput(options) {
115
+ return new Output({
116
+ json: options.json || options.parent?.opts()?.json || false
117
+ });
118
+ }
119
+
120
+ module.exports = {
121
+ Output,
122
+ createOutput
123
+ };
@@ -0,0 +1,89 @@
1
+ const { poseidon1 } = require('poseidon-lite');
2
+
3
+ /**
4
+ * Compute PIN hash using Poseidon hash function
5
+ * This hash is stored on-chain during contract creation
6
+ * @param {string|number} pin - The PIN to hash
7
+ * @returns {string} - Hex string of the hash (bytes32)
8
+ */
9
+ function computePinHash(pin) {
10
+ const pinBigInt = BigInt(pin);
11
+ const hash = poseidon1([pinBigInt]);
12
+ return '0x' + hash.toString(16).padStart(64, '0');
13
+ }
14
+
15
+ /**
16
+ * Generate zkSNARK proof for PIN verification
17
+ * Used for account management operations (add/remove/replace owner)
18
+ * @param {string|number} pin - The PIN
19
+ * @param {string} pinHash - The stored PIN hash from contract
20
+ * @param {number} nonce - Current nonce from contract (unused - kept for API compatibility)
21
+ * @param {string} sender - The sender address (msg.sender)
22
+ * @returns {Object} - Proof components { pA, pB, pC }
23
+ */
24
+ async function generateZkProof(pin, pinHash, nonce, sender) {
25
+ const snarkjs = require('snarkjs');
26
+ const path = require('path');
27
+ const fs = require('fs');
28
+
29
+ // Paths to circuit artifacts
30
+ const wasmPath = path.join(__dirname, '../../zkbuild/pin_verify_js/pin_verify.wasm');
31
+ const zkeyPath = path.join(__dirname, '../../zkbuild/pin_verify_final.zkey');
32
+
33
+ // Check if circuit artifacts exist
34
+ if (!fs.existsSync(wasmPath) || !fs.existsSync(zkeyPath)) {
35
+ throw new Error('zkSNARK circuit artifacts not found. Run compile_circuit.sh first.');
36
+ }
37
+
38
+ // Convert sender address to BigInt (remove 0x prefix)
39
+ const senderBigInt = BigInt(sender).toString();
40
+
41
+ // Prepare inputs - circuit expects: pinHash, sender (public) + pin (private)
42
+ // Note: nonce was removed from circuit for better UX
43
+ const input = {
44
+ pin: BigInt(pin).toString(),
45
+ pinHash: pinHash,
46
+ sender: senderBigInt
47
+ };
48
+
49
+ // Generate proof
50
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(
51
+ input,
52
+ wasmPath,
53
+ zkeyPath
54
+ );
55
+
56
+ // Format proof for Solidity verifier
57
+ const pA = [proof.pi_a[0], proof.pi_a[1]];
58
+ const pB = [
59
+ [proof.pi_b[0][1], proof.pi_b[0][0]],
60
+ [proof.pi_b[1][1], proof.pi_b[1][0]]
61
+ ];
62
+ const pC = [proof.pi_c[0], proof.pi_c[1]];
63
+
64
+ return { pA, pB, pC, publicSignals };
65
+ }
66
+
67
+ /**
68
+ * Verify a proof locally (for testing)
69
+ */
70
+ async function verifyProofLocally(proof, publicSignals) {
71
+ const snarkjs = require('snarkjs');
72
+ const path = require('path');
73
+ const fs = require('fs');
74
+
75
+ const vkeyPath = path.join(__dirname, '../../zkbuild/verification_key.json');
76
+
77
+ if (!fs.existsSync(vkeyPath)) {
78
+ throw new Error('Verification key not found.');
79
+ }
80
+
81
+ const vkey = JSON.parse(fs.readFileSync(vkeyPath, 'utf8'));
82
+ return await snarkjs.groth16.verify(vkey, publicSignals, proof);
83
+ }
84
+
85
+ module.exports = {
86
+ computePinHash,
87
+ generateZkProof,
88
+ verifyProofLocally
89
+ };