diff --git a/backends/CoreLightningRequestHandler.ts b/backends/CoreLightningRequestHandler.ts index 78e12a34b..e9349a5f5 100644 --- a/backends/CoreLightningRequestHandler.ts +++ b/backends/CoreLightningRequestHandler.ts @@ -1,4 +1,5 @@ import CLNRest from './CLNRest'; +import AddressUtils from '../utils/AddressUtils'; const api = new CLNRest(); @@ -139,7 +140,20 @@ export const getChainTransactions = async () => { api.postRequest('/v1/listtransactions'), api.postRequest('/v1/getinfo') ]); - const [sqlResult, listTxsResult, getinfoResult] = results; + const [sqlResult, listTxsResult, getinfoResult]: any = results; + + listTxsResult?.value?.transactions?.forEach((tx: any) => { + const addresses: Array = []; + tx.outputs.forEach((output: any) => { + try { + const address = AddressUtils.scriptPubKeyToAddress( + output.scriptPubKey + ); + if (address) addresses.push(address); + } catch (e) {} + }); + tx.dest_addresses = addresses; + }); // If getinfo fails, return blank txs if (getinfoResult.status !== 'fulfilled') { @@ -218,7 +232,7 @@ export const getChainTransactions = async () => { num_confirmations: getinfo.blockheight - withdrawal[6], time_stamp: withdrawal[5], txid: tx.hash, - note: 'on-chain withdrawal' + dest_addresses: tx.dest_addresses }; } @@ -229,7 +243,7 @@ export const getChainTransactions = async () => { num_confirmations: getinfo.blockheight - deposit[6], time_stamp: deposit[5], txid: tx.hash, - note: 'on-chain deposit' + dest_addresses: tx.dest_addresses }; } diff --git a/locales/en.json b/locales/en.json index af8098fba..343387f39 100644 --- a/locales/en.json +++ b/locales/en.json @@ -957,7 +957,6 @@ "views.Transaction.title": "Transaction", "views.Transaction.totalFees": "Total Fees", "views.Transaction.transactionHash": "Transaction Hash", - "views.Transaction.note": "Transaction Note", "views.Transaction.blockHash": "Block Hash", "views.Transaction.blockHeight": "Block Height", "views.Transaction.numConf": "Number of Confirmations", diff --git a/models/Transaction.ts b/models/Transaction.ts index 05168aedd..5d658e155 100644 --- a/models/Transaction.ts +++ b/models/Transaction.ts @@ -119,7 +119,11 @@ export default class Transaction extends BaseModel { } @computed public get destAddresses(): Array { - return this.dest_addresses || [this.address]; + return this.dest_addresses + ? this.dest_addresses + : this.address + ? [this.address] + : []; } @computed public get getOutpoint(): string { diff --git a/package.json b/package.json index ed39b7a78..085fc9915 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@keystonehq/bc-ur-registry": "0.7.0", "@lightninglabs/lnc-core": "file:zeus_modules/@lightninglabs/lnc-core", "@ngraveio/bc-ur": "1.1.12", + "@noble/secp256k1": "1.6.3", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-clipboard/clipboard": "1.14.3", "@react-native-community/netinfo": "11.3.2", @@ -49,6 +50,7 @@ "bech32": "2.0.0", "big-integer": "1.6.52", "bignumber.js": "9.0.2", + "bip32": "3.0.1", "bip39": "3.1.0", "bitcoinjs-lib": "6.1.5", "bolt11": "1.4.1", @@ -62,6 +64,7 @@ "dateformat": "5.0.3", "dns.js": "1.0.1", "domain-browser": "1.2.0", + "ecpair": "2.0.1", "elliptic": "6.6.0", "events": "1.1.1", "fast-sha256": "1.3.0", @@ -148,6 +151,7 @@ "@react-native/babel-preset": "0.74.84", "@react-native/eslint-config": "0.74.84", "@react-native/metro-config": "0.74.84", + "@types/create-hash": "1.2.2", "@types/crypto-js": "4.1.1", "@types/jest": "29.5.12", "@types/lodash": "4.14.198", diff --git a/utils/AddressUtils-testnet.test.ts b/utils/AddressUtils-testnet.test.ts new file mode 100644 index 000000000..5bb992a34 --- /dev/null +++ b/utils/AddressUtils-testnet.test.ts @@ -0,0 +1,150 @@ +jest.mock('react-native-encrypted-storage', () => ({ + setItem: jest.fn(() => Promise.resolve()), + getItem: jest.fn(() => Promise.resolve()), + removeItem: jest.fn(() => Promise.resolve()), + clear: jest.fn(() => Promise.resolve()) +})); + +jest.mock('../stores/Stores', () => ({ + SettingsStore: { + settings: { + display: { + removeDecimalSpaces: false + } + } + }, + nodeInfoStore: { + nodeInfo: { + isTestNet: true, + isRegTest: false + } + } +})); + +import AddressUtils from './AddressUtils'; + +describe('AddressUtils', () => { + describe('scriptPubKeyToAddress', () => { + test('should correctly decode a P2WSH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = + '002008737603b10129fc2dcb2e5167eb556ba2c84aec6622ff4d46767d186f63150d'; + expectedAddress = + 'tb1qppehvqa3qy5lctwt9egk0664dw3vsjhvvc307n2xwe73smmrz5xsrsq8yw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020a1157ce5620e1e93ad8a98a9765971c89b5920a343add041b47ec34b7302d951'; + expectedAddress = + 'tb1q5y2heetzpc0f8tv2nz5hvkt3ezd4jg9rgwkaqsd50mp5kuczm9gssy38uc'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020983cca35d586a96de538166552d2773ed291b96a3af65917560ee7eda5e9e106'; + expectedAddress = + 'tb1qnq7v5dw4s65kmefczej495nh8mffrwt28tm9j96kpmn7mf0fuyrqs8aldv'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020c273ca3b47fcab3a53c9cc4daacfb37d921e62014c794f6d25606f43797fc0f0'; + expectedAddress = + 'tb1qcfeu5w68lj4n557fe3x64nan0kfpucspf3u57mf9vph5x7tlcrcquq4gza'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2SH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = 'a91426101b3dae044fddcd71e6dfe831ebe383f23a5887'; + expectedAddress = '2MviUxfcKQdqszQPxofWjY4gCFQifcfG31b'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a914b40fce0fca1eefcec03e61e833bf6326c11ccaf087'; + expectedAddress = '2N9fJYxagvUGVCcN4utpBhRMgG7eLAjZT9F'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a9142d2ecbffc89a98365e2b45e90f00d3f6edec68d687'; + expectedAddress = '2MwN8TRK2jz44dBxvcebZzbqAYLC1MV3e3N'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2WPKH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '00201ff7ed9fecf23980cb3e6d9db8331054942aee8ca34b7190450e20a69ffeeda2'; + expectedAddress = + 'tb1qrlm7m8lv7gucpje7dkwmsvcs2j2z4m5v5d9hryz9pcs2d8l7ak3q2c7047'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014def7b24f3e42b0858240cf3b993a3d44a7f5abe9'; + expectedAddress = 'tb1qmmmmyne7g2cgtqjqeuaejw3agjnlt2lftcxm8w'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014e8550bc74d38fd002481339349ed4780cb1d77b3'; + expectedAddress = 'tb1qap2sh36d8r7sqfypxwf5nm28sr936aan8980fa'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014647f217856160110ce3abcc8cc2ec77ff39561ad'; + expectedAddress = 'tb1qv3ljz7zkzcq3pn36hnyvctk80lee2cddknywz0'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2TR scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '5120fb3c4af6e6471fe9319afcfd25eb3daf2b83e5be7411b1932bb36ed84701337f'; + expectedAddress = + 'tb1plv7y4ahxgu07jvv6ln7jt6ea4u4c8ed7wsgmryetkdhds3cpxdlsp8yh6m'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '512071d18973aa5daf214d500de76c1b860fcac1228260af21a62d5f6eed74cb0547'; + expectedAddress = + 'tb1pw8gcjua2tkhjzn2sphnkcxuxpl9vzg5zvzhjrf3dtahw6axtq4rsnn9w4l'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120bd6b2524312d1ce75e7e00676b1da2f6b72a846ca767b3009f09b471623a5865'; + expectedAddress = + 'tb1ph44j2fp395wwwhn7qpnkk8dz76mj4prv5anmxqylpx68zc36tpjs7hyrpn'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120ad55091d54ca1938cebe01f5435790c352021df23d8153a301cd90d06171ceed'; + expectedAddress = + 'tb1p442sj825egvn3n47q865x4uscdfqy80j8kq48gcpekgdqct3emksxu4anx'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + }); +}); diff --git a/utils/AddressUtils.test.ts b/utils/AddressUtils.test.ts index d968bd574..c54ee3c2d 100644 --- a/utils/AddressUtils.test.ts +++ b/utils/AddressUtils.test.ts @@ -12,6 +12,12 @@ jest.mock('../stores/Stores', () => ({ removeDecimalSpaces: false } } + }, + nodeInfoStore: { + nodeInfo: { + isTestNet: false, + isRegTest: false + } } })); @@ -965,4 +971,142 @@ describe('AddressUtils', () => { ).toEqual('Unused taproot pubkey'); }); }); + + describe('scriptPubKeyToAddress', () => { + test('should correctly decode a P2WSH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = + '002008737603b10129fc2dcb2e5167eb556ba2c84aec6622ff4d46767d186f63150d'; + expectedAddress = + 'bc1qppehvqa3qy5lctwt9egk0664dw3vsjhvvc307n2xwe73smmrz5xs5ckg7p'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020a1157ce5620e1e93ad8a98a9765971c89b5920a343add041b47ec34b7302d951'; + expectedAddress = + 'bc1q5y2heetzpc0f8tv2nz5hvkt3ezd4jg9rgwkaqsd50mp5kuczm9gs8v8gxh'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020983cca35d586a96de538166552d2773ed291b96a3af65917560ee7eda5e9e106'; + expectedAddress = + 'bc1qnq7v5dw4s65kmefczej495nh8mffrwt28tm9j96kpmn7mf0fuyrq80tshr'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020c273ca3b47fcab3a53c9cc4daacfb37d921e62014c794f6d25606f43797fc0f0'; + expectedAddress = + 'bc1qcfeu5w68lj4n557fe3x64nan0kfpucspf3u57mf9vph5x7tlcrcqtgr8cj'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2SH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = 'a91426101b3dae044fddcd71e6dfe831ebe383f23a5887'; + expectedAddress = '35AGtvgHoBLXncmR8Xtrv7gw34WVqkLLeU'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a914b40fce0fca1eefcec03e61e833bf6326c11ccaf087'; + expectedAddress = '3J76VDefK1m8zpjXEmCK5UNR3mSAN7BcBD'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a9142d2ecbffc89a98365e2b45e90f00d3f6edec68d687'; + expectedAddress = '35ovPgP18XYiRQLNwWyhNequKyyqavs3Sw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2WPKH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '00201ff7ed9fecf23980cb3e6d9db8331054942aee8ca34b7190450e20a69ffeeda2'; + expectedAddress = + 'bc1qrlm7m8lv7gucpje7dkwmsvcs2j2z4m5v5d9hryz9pcs2d8l7ak3qasgq03'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014def7b24f3e42b0858240cf3b993a3d44a7f5abe9'; + expectedAddress = 'bc1qmmmmyne7g2cgtqjqeuaejw3agjnlt2lfp7agua'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014e8550bc74d38fd002481339349ed4780cb1d77b3'; + expectedAddress = 'bc1qap2sh36d8r7sqfypxwf5nm28sr936aandruujw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014647f217856160110ce3abcc8cc2ec77ff39561ad'; + expectedAddress = 'bc1qv3ljz7zkzcq3pn36hnyvctk80lee2cddu4laeu'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2TR scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '5120fb3c4af6e6471fe9319afcfd25eb3daf2b83e5be7411b1932bb36ed84701337f'; + expectedAddress = + 'bc1plv7y4ahxgu07jvv6ln7jt6ea4u4c8ed7wsgmryetkdhds3cpxdlsk0jcq5'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '512071d18973aa5daf214d500de76c1b860fcac1228260af21a62d5f6eed74cb0547'; + expectedAddress = + 'bc1pw8gcjua2tkhjzn2sphnkcxuxpl9vzg5zvzhjrf3dtahw6axtq4rsymnp0s'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120bd6b2524312d1ce75e7e00676b1da2f6b72a846ca767b3009f09b471623a5865'; + expectedAddress = + 'bc1ph44j2fp395wwwhn7qpnkk8dz76mj4prv5anmxqylpx68zc36tpjsfljvmu'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120ad55091d54ca1938cebe01f5435790c352021df23d8153a301cd90d06171ceed'; + expectedAddress = + 'bc1p442sj825egvn3n47q865x4uscdfqy80j8kq48gcpekgdqct3emks35rjff'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should throw an error for an invalid scriptPubKey (non-hex input)', () => { + const invalidScriptPubKey = 'invalid_script'; + expect(() => { + AddressUtils.scriptPubKeyToAddress(invalidScriptPubKey); + }).toThrow('Unknown scriptPubKey format'); + }); + + test('should throw an error for empty scriptPubKey', () => { + const emptyScriptPubKey = ''; + expect(() => { + AddressUtils.scriptPubKeyToAddress(emptyScriptPubKey); + }).toThrow('Unknown scriptPubKey format'); + }); + }); }); diff --git a/utils/AddressUtils.ts b/utils/AddressUtils.ts index 5882a2a22..5651e998b 100644 --- a/utils/AddressUtils.ts +++ b/utils/AddressUtils.ts @@ -1,7 +1,11 @@ import BigNumber from 'bignumber.js'; -const bitcoin = require('bitcoinjs-lib'); +import * as bitcoin from 'bitcoinjs-lib'; +import ecc from '../zeus_modules/noble_ecc'; + +bitcoin.initEccLib(ecc); import Base64Utils from '../utils/Base64Utils'; +import stores from '../stores/Stores'; import { SATS_PER_BTC } from '../utils/UnitsUtils'; @@ -327,6 +331,153 @@ class AddressUtils { } return output; }; + + scriptPubKeyToAddress = (scriptPubKeyHex: string) => { + const nodeInfo = stores?.nodeInfoStore?.nodeInfo; + const { isTestNet, isRegTest } = nodeInfo; + + let network = bitcoin.networks.bitcoin; + if (isTestNet) network = bitcoin.networks.testnet; + if (isRegTest) network = bitcoin.networks.regtest; + + const scriptBuffer = Buffer.from(scriptPubKeyHex, 'hex'); + + const decodedScript = bitcoin.script.decompile(scriptBuffer); + + // Handle P2WSH (Pay-to-Witness-Script-Hash) + if ( + decodedScript && // Ensure the script is decoded + decodedScript.length === 2 && // P2WSH scripts have exactly 2 elements + decodedScript[0] === bitcoin.opcodes.OP_0 && // First element is OP_0 + Buffer.isBuffer(decodedScript[1]) && // Second element is push data (a Buffer) + decodedScript[1].length === 32 // Push data length is exactly 32 bytes + ) { + try { + const witnessProgram = decodedScript[1]; + const { address } = bitcoin.payments.p2wsh({ + hash: witnessProgram, + network + }); + return address; + } catch (e) { + console.log( + `error decoding P2WSH pkscript: ${scriptPubKeyHex}:`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2PKH (Pay-to-PubKey-Hash) + // TODO add address validation + tests + if ( + decodedScript && + decodedScript[0] === bitcoin.opcodes.OP_DUP && + decodedScript[1] === bitcoin.opcodes.OP_HASH160 && + Buffer.isBuffer(decodedScript[2]) && // Ensure it's a valid public key hash (20 bytes) + decodedScript[2].length === 20 + ) { + console.log('attempting to decode P2PKH pkscript'); + try { + const pubKeyHash = decodedScript[2]; + const { address } = bitcoin.payments.p2pkh({ + hash: pubKeyHash, + network + }); + console.log('P2PKH address', address); + return address; + } catch (e) { + console.log( + `error decoding P2PKH pkscript: ${scriptPubKeyHex}:`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2PK (Pay-to-PubKey) + // TODO add address validation + tests + if (decodedScript && decodedScript[1] === bitcoin.opcodes.OP_CHECKSIG) { + console.log('attempting to decode P2PK pkscript'); + try { + const pubkey: any = decodedScript[0]; + const { address } = bitcoin.payments.p2pk({ + pubkey, + network + }); + console.log('P2PK address', address); + return address; + } catch (e) { + console.log( + `error decoding P2PK pkscript: ${scriptPubKeyHex}`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2SH (Pay-to-Script-Hash) + if (decodedScript && decodedScript[0] === bitcoin.opcodes.OP_HASH160) { + try { + const scriptHash: any = decodedScript[1]; + const { address } = bitcoin.payments.p2sh({ + hash: scriptHash, + network + }); + return address; + } catch (e) { + console.log( + `error decoding P2SH pkscript: ${scriptPubKeyHex}`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2WPKH (Pay-to-Witness-PubKey-Hash) - SegWit + if ( + decodedScript && + decodedScript[0] === bitcoin.opcodes.OP_0 && // First element is OP_0 + Buffer.isBuffer(decodedScript[1]) && // Second element is a buffer (push data) + decodedScript[1].length === 20 // Push data is exactly 20 bytes + ) { + try { + const pubKeyHash = decodedScript[1]; // Use the hash part, not the entire script + const { address } = bitcoin.payments.p2wpkh({ + hash: pubKeyHash, + network + }); + return address; + } catch (e) { + console.log( + `error decoding P2WPKH pkscript: ${scriptPubKeyHex}`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2TR (Pay-to-Taproot) - Taproot address + if (decodedScript && decodedScript[0] === 0x51) { + try { + const taprootPubKey: any = decodedScript[1]; + const { address } = bitcoin.payments.p2tr({ + pubkey: taprootPubKey, + network + }); + return address; + } catch (e) { + console.log( + `error decoding P2TR pkscript: ${scriptPubKeyHex}`, + e + ); + throw new Error('Invalid scriptPubKey format'); + } + } + + console.log('unknown address type for script pubkey', scriptPubKeyHex); + throw new Error('Unknown scriptPubKey format'); + }; } const addressUtils = new AddressUtils(); diff --git a/views/Transaction.tsx b/views/Transaction.tsx index 0733c18c8..2d7f949ee 100644 --- a/views/Transaction.tsx +++ b/views/Transaction.tsx @@ -76,7 +76,6 @@ export default class TransactionView extends React.Component< num_confirmations, time_stamp, destAddresses, - note, getFee, getFeePercentage, status, @@ -322,21 +321,6 @@ export default class TransactionView extends React.Component< {!!destAddresses && ( {addresses} )} - {!!note && ( - - {note} - - } - /> - )} {raw_tx_hex && ( <> diff --git a/yarn.lock b/yarn.lock index 70dc05baf..c72b0c0e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2029,6 +2029,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/secp256k1@1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" + integrity sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3070,6 +3075,13 @@ dependencies: "@types/node" "*" +"@types/create-hash@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/create-hash/-/create-hash-1.2.2.tgz#e87247083df8478f6b83655592bde0d709028235" + integrity sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ== + dependencies: + "@types/node" "*" + "@types/crypto-js@4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" @@ -3171,6 +3183,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@10.12.18": + version "10.12.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" + integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== + "@types/node@^17.0.0": version "17.0.45" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" @@ -3986,6 +4003,18 @@ bip174@^2.1.1: resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.1.tgz#ef3e968cf76de234a546962bcf572cc150982f9f" integrity sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ== +bip32@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-3.0.1.tgz#1d1121469cce6e910e0ec3a5a1990dd62687e2a3" + integrity sha512-Uhpp9aEx3iyiO7CpbNGFxv9WcMIVdGoHG04doQ5Ln0u60uwDah7jUSc3QMV/fSZGm/Oo01/OeAmYevXV+Gz5jQ== + dependencies: + "@types/node" "10.12.18" + bs58check "^2.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + typeforce "^1.11.5" + wif "^2.0.6" + bip39@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" @@ -4206,7 +4235,7 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@2.1.2, bs58check@^2.1.2: +bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -5044,6 +5073,15 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +ecpair@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ecpair/-/ecpair-2.0.1.tgz#e25ab416f1ecb6b05477ca601313df937b075d2e" + integrity sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw== + dependencies: + randombytes "^2.1.0" + typeforce "^1.18.0" + wif "^2.0.6" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -10258,7 +10296,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typeforce@^1.11.3: +typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -10549,6 +10587,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wif@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ== + dependencies: + bs58check "<3.0.0" + word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" diff --git a/zeus_modules/noble_ecc.ts b/zeus_modules/noble_ecc.ts new file mode 100644 index 000000000..061b259c4 --- /dev/null +++ b/zeus_modules/noble_ecc.ts @@ -0,0 +1,145 @@ +/** + * adapted from https://github.com/BitGo/BitGoJS/blob/bitcoinjs_lib_6_sync/modules/utxo-lib/src/noble_ecc.ts + * license: Apache License + * + * @see https://github.com/bitcoinjs/tiny-secp256k1/issues/84#issuecomment-1185682315 + * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/1781 + */ +import * as necc from '@noble/secp256k1'; +import { TinySecp256k1Interface as TinySecp256k1InterfaceBIP32 } from 'bip32/types/bip32'; +import { XOnlyPointAddTweakResult } from 'bitcoinjs-lib/src/types'; +import createHash from 'create-hash'; +import { createHmac } from 'crypto'; +import { TinySecp256k1Interface } from 'ecpair/src/ecpair'; + +export interface TinySecp256k1InterfaceExtended { + pointMultiply(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null; + + pointAdd(pA: Uint8Array, pB: Uint8Array, compressed?: boolean): Uint8Array | null; + + isXOnlyPoint(p: Uint8Array): boolean; + + xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; + + privateNegate(d: Uint8Array): Uint8Array; +} + +necc.utils.sha256Sync = (...messages: Uint8Array[]): Uint8Array => { + const sha256 = createHash('sha256'); + for (const message of messages) sha256.update(message); + return sha256.digest(); +}; + +necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8Array => { + const hash = createHmac('sha256', Buffer.from(key)); + messages.forEach(m => hash.update(m)); + return Uint8Array.from(hash.digest()); +}; + +/* const normal = necc.utils._normalizePrivateKey; +type Hex = string | Uint8Array; +type PrivKey = Hex | bigint | number; + +necc.utils.privateAdd = (privateKey: PrivKey, tweak: Hex) => { + console.log({ privateKey, tweak }); + const p = normal(privateKey); + const t = normal(tweak); + return necc.utils.privateAdd(necc.utils.mod(p + t, necc.CURVE.n)); +}; */ + +const defaultTrue = (param?: boolean): boolean => param !== false; + +function throwToNull(fn: () => Type): Type | null { + try { + return fn(); + } catch (e) { + // console.log(e); + return null; + } +} + +function isPoint(p: Uint8Array, xOnly: boolean): boolean { + if ((p.length === 32) !== xOnly) return false; + try { + return !!necc.Point.fromHex(p); + } catch (e) { + return false; + } +} + +const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256k1InterfaceBIP32 = { + isPoint: (p: Uint8Array): boolean => isPoint(p, false), + isPrivate: (d: Uint8Array): boolean => { + /* if ( + [ + '0000000000000000000000000000000000000000000000000000000000000000', + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142', + ].includes(d.toString('hex')) + ) { + return false; + } */ + return necc.utils.isValidPrivateKey(d); + }, + isXOnlyPoint: (p: Uint8Array): boolean => isPoint(p, true), + + xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array): { parity: 0 | 1; xOnlyPubkey: Uint8Array } | null => + throwToNull(() => { + const P = necc.utils.pointAddScalar(p, tweak, true); + const parity = P[0] % 2 === 1 ? 1 : 0; + return { parity, xOnlyPubkey: P.slice(1) }; + }), + + pointFromScalar: (sk: Uint8Array, compressed?: boolean): Uint8Array | null => + throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))), + + pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => { + return necc.Point.fromHex(p).toRawBytes(defaultTrue(compressed)); + }, + + pointMultiply: (a: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null => + throwToNull(() => necc.utils.pointMultiply(a, tweak, defaultTrue(compressed))), + + pointAdd: (a: Uint8Array, b: Uint8Array, compressed?: boolean): Uint8Array | null => + throwToNull(() => { + const A = necc.Point.fromHex(a); + const B = necc.Point.fromHex(b); + return A.add(B).toRawBytes(defaultTrue(compressed)); + }), + + pointAddScalar: (p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null => + throwToNull(() => necc.utils.pointAddScalar(p, tweak, defaultTrue(compressed))), + + privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null => + throwToNull(() => { + // console.log({ d, tweak }); + const ret = necc.utils.privateAdd(d, tweak); + // console.log(ret); + if (ret.join('') === '00000000000000000000000000000000') { + return null; + } + return ret; + }), + + privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d), + + sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => { + return necc.signSync(h, d, { der: false, extraEntropy: e }); + }, + + signSchnorr: (h: Uint8Array, d: Uint8Array, e: Uint8Array = Buffer.alloc(32, 0x00)): Uint8Array => { + return necc.schnorr.signSync(h, d, e); + }, + + verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean => { + return necc.verify(signature, h, Q, { strict }); + }, + + verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => { + return necc.schnorr.verifySync(signature, h, Q); + }, +}; + +export default ecc; + +// module.exports.ecc = ecc;