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.
- package/README.md +514 -0
- package/cli/commands/account/index.js +855 -0
- package/cli/commands/config/index.js +369 -0
- package/cli/commands/contact/index.js +139 -0
- package/cli/commands/contract/index.js +197 -0
- package/cli/commands/data/index.js +536 -0
- package/cli/commands/tx/index.js +841 -0
- package/cli/commands/wallet/index.js +181 -0
- package/cli/index.js +624 -0
- package/cli/utils/auth.js +146 -0
- package/cli/utils/constants.js +68 -0
- package/cli/utils/contacts.js +131 -0
- package/cli/utils/contracts.js +269 -0
- package/cli/utils/crosschain.js +278 -0
- package/cli/utils/networks.js +335 -0
- package/cli/utils/notifications.js +135 -0
- package/cli/utils/output.js +123 -0
- package/cli/utils/pin.js +89 -0
- package/data/balance.js +680 -0
- package/data/events.js +334 -0
- package/data/pending.js +261 -0
- package/data/scanWorker.js +169 -0
- package/data/token_cache.json +54 -0
- package/data/token_database.json +92 -0
- package/data/tokens.js +380 -0
- package/package.json +57 -0
|
@@ -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
|
+
};
|
package/cli/utils/pin.js
ADDED
|
@@ -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
|
+
};
|