diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml index 1faea836b4..d4429d3efc 100644 --- a/.idea/dictionaries/fuxing.xml +++ b/.idea/dictionaries/fuxing.xml @@ -208,6 +208,7 @@ walletversion wpkh wtxid + xpubkey zynesis diff --git a/packages/jellyfish-wallet-encrypted/__tests__/bip32.test.ts b/packages/jellyfish-wallet-encrypted/__tests__/bip32.test.ts new file mode 100644 index 0000000000..33058b385f --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/__tests__/bip32.test.ts @@ -0,0 +1,304 @@ +import BigNumber from 'bignumber.js' +import { EncryptedHdNodeProvider, EncryptedMnemonicHdNode, Scrypt, SimpleScryptsy } from '../src' +import { MnemonicHdNode, MnemonicHdNodeProvider } from '@defichain/jellyfish-wallet-mnemonic' +import { OP_CODES, Transaction, Vout } from '@defichain/jellyfish-transaction' +import { HASH160 } from '@defichain/jellyfish-crypto' + +const regTestBip32Options = { + bip32: { + public: 0x043587cf, + private: 0x04358394 + }, + wif: 0xef +} + +const transaction: Transaction = { + version: 0x00000004, + lockTime: 0x00000000, + vin: [{ + index: 0, + script: { stack: [] }, + sequence: 4294967278, + txid: '9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff' + }], + vout: [{ + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(Buffer.from('1d0f172a0ecb48aee1be1f2687d2963ae33f71a1', 'hex'), 'little') + ] + }, + value: new BigNumber('5.98'), + tokenId: 0x00 + }] +} + +const prevout: Vout = { + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(Buffer.from('1d0f172a0ecb48aee1be1f2687d2963ae33f71a1', 'hex'), 'little') + ] + }, + value: new BigNumber('6'), + tokenId: 0x00 +} + +describe('24 words: random with passphrase "random" (exact same test in jellyfish-wallet-mnemonic)', () => { + const scrypt = new Scrypt(new SimpleScryptsy({ N: 16384, r: 8, p: 1 })) + let provider: EncryptedHdNodeProvider + + beforeAll(() => { + const words = MnemonicHdNodeProvider.generateWords(24) + const passphrase = 'random' + const data = EncryptedHdNodeProvider.wordsToEncryptedData( + words, + regTestBip32Options, + scrypt, + passphrase + ) + provider = EncryptedHdNodeProvider.init(data, regTestBip32Options, scrypt, async () => passphrase) + }) + + describe("44'/1129'/0'/0/0", () => { + let node: EncryptedMnemonicHdNode + + beforeEach(() => { + node = provider.derive("44'/1129'/0'/0/0") + }) + + it('should not derive pub key because hardened', async () => { + const promise = node.publicKey() + await expect(promise).rejects.toThrowError('Missing private key for hardened child key') + }) + + it('should derive priv key', async () => { + const derivedPrivKey = await node.privateKey() + expect(derivedPrivKey.length).toStrictEqual(32) + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.length).toBeLessThanOrEqual(70) + expect(signature.length).toBeGreaterThanOrEqual(67) // 0.00001 probability of being this length + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + }) + + describe('0/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('0/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.length).toStrictEqual(33) + }) + + it('should derive priv key', async () => { + const derivedPrivKey = await node.privateKey() + expect(derivedPrivKey.length).toStrictEqual(32) + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.length).toBeLessThanOrEqual(70) + expect(signature.length).toBeGreaterThanOrEqual(67) // 0.00001 probability of being this length + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex.length).toBeGreaterThanOrEqual(140) + expect(signed.witness[0].scripts[0].hex.length).toBeLessThanOrEqual(142) + expect(signed.witness[0].scripts[1].hex.length).toStrictEqual(66) + }) + }) +}) + +describe('24 words: abandon x23 art with passphrase "jellyfish-wallet-encrypted" (exact same test in jellyfish-wallet-mnemonic)', () => { + const scrypt = new Scrypt(new SimpleScryptsy({ N: 16384, r: 8, p: 1 })) + let provider: EncryptedHdNodeProvider + + beforeAll(() => { + const words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'.split(' ') + const passphrase = 'jellyfish-wallet-encrypted' + const data = EncryptedHdNodeProvider.wordsToEncryptedData( + words, + regTestBip32Options, + scrypt, + passphrase + ) + provider = EncryptedHdNodeProvider.init(data, regTestBip32Options, scrypt, async () => passphrase) + }) + + describe("44'/1129'/0'/0/0", () => { + let node: EncryptedMnemonicHdNode + + beforeEach(() => { + node = provider.derive("44'/1129'/0'/0/0") + }) + + it('should not derive pub key because hardened', async () => { + const promise = node.publicKey() + await expect(promise).rejects.toThrowError('Missing private key for hardened child key') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('3e1f9339b4685c35d590fd1a6801967a9f95dbedf3e5733efa6451dc771a2d18') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('3044022070454813f8ff8e7a13f2ef9be18c89a3768d846647559798c147cd2ae284d1b1022058584df9e77efd620c7657f8d63eb7a2cd8c5753e3d29bc50bcb4c8c5c95ce49') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + }) + + describe("44'/1129'/1'/0/0", () => { + let node: EncryptedMnemonicHdNode + + beforeEach(() => { + node = provider.derive("44'/1129'/1'/0/0") + }) + + it('should derive pub key', async () => { + const promise = node.publicKey() + await expect(promise).rejects.toThrowError('Missing private key for hardened child key') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('be7b3f86469900fc9302cea6bcf3b05c165a6461f8a0e7796305c350fc1f7357') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('304402201866354d84fb7b576c3a3248adb55582aa9a1c61b8d27dc355c4d9d07aa16b480220311133b0a69ab54a63406b1fce001c91d8a65ef665016d9792850edbe34a7598') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + }) + + describe('0/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('0/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.toString('hex')).toStrictEqual('03d0d24a126c861c02622cb4ee75b860d6e65d1d6ffee20bf5793c1a00ade37db5') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('209fab36379401ac23960f0890ac1cc880e9c6e351a8525dac59a3ea6bb4ebb7') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('3044022007340e6a1052ca16acb180f33dd6781c60e2aa1acd27f4b50c13c6f370c9c8a802206fca4fd8263eb84d6e2c8d6111a5663b5da59eec843aab5cbfcbb273d497917c') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex).toStrictEqual('304402202c7ba3ded9cb503e8afb8a14ddb16ef4ea66043157e2a76e40d5d534fa80114302202aa1ea35654e2cd59a9f2e325037b38ef3d3939b47041782f05c819b1090dbd201') + expect(signed.witness[0].scripts[1].hex).toStrictEqual('03d0d24a126c861c02622cb4ee75b860d6e65d1d6ffee20bf5793c1a00ade37db5') + }) + }) + + describe('1/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('1/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.toString('hex')).toStrictEqual('0332b504dca50e2f9f369ac3bdc1a29fc5a082c9a35cc60f54d4c115518bb7a824') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('12137509a9c70bc0ac55f0897577ba55cd1c222a209096f7b27f3de4c1eef30d') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('3044022036a7d5ad851541745fa4081327ef846c262d46640cbb1f5d989c07db3400fe4a0220384de187749c731ea47036d4f4bb77db69961359b8c70a1da5750b9acec2e573') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex).toStrictEqual('30440220729650b5ab2e325e13da49e474fa5f434f7c68f2d62a734e48c982a921b38830022069c02966ede1db87ac726326da7d5467775de23ec56516f151b31abbb90ec7e701') + expect(signed.witness[0].scripts[1].hex).toStrictEqual('0332b504dca50e2f9f369ac3bdc1a29fc5a082c9a35cc60f54d4c115518bb7a824') + }) + }) +}) diff --git a/packages/jellyfish-wallet-encrypted/__tests__/scrypt.test.ts b/packages/jellyfish-wallet-encrypted/__tests__/scrypt.test.ts index 5e235d7723..a47f44d739 100644 --- a/packages/jellyfish-wallet-encrypted/__tests__/scrypt.test.ts +++ b/packages/jellyfish-wallet-encrypted/__tests__/scrypt.test.ts @@ -12,9 +12,7 @@ it('should be able to encrypt / decrypt', async () => { expect(data.encode()).not.toStrictEqual(null) const encoded = data.encode() - const hash = data.hash.toString('hex') - - const decrypted = await scrypt.decrypt(encoded, passphrase, hash) + const decrypted = await scrypt.decrypt(encoded, passphrase) expect(decrypted).toStrictEqual(buffer) expect(decrypted.toString('hex')).toStrictEqual(privKey) }) @@ -29,9 +27,7 @@ it('should be able to encrypt / decrypt - simple passphrase, a 6 digit pin', asy expect(data.encode()).not.toStrictEqual(null) const encoded = data.encode() - const hash = data.hash.toString('hex') - - const decrypted = await scrypt.decrypt(encoded, passphrase, hash) + const decrypted = await scrypt.decrypt(encoded, passphrase) expect(decrypted).toStrictEqual(buffer) expect(decrypted.toString('hex')).toStrictEqual(privKey) }) @@ -46,9 +42,7 @@ it('Should work with variable data length - long', async () => { expect(data.encode()).not.toStrictEqual(null) const encoded = data.encode() - const hash = data.hash.toString('hex') - - const decrypted = await scrypt.decrypt(encoded, passphrase, hash) + const decrypted = await scrypt.decrypt(encoded, passphrase) expect(decrypted).toStrictEqual(buffer) expect(decrypted.toString('hex')).toStrictEqual(privKey) }) @@ -63,9 +57,7 @@ it('should work with variable data length - short', async () => { expect(data.encode()).not.toStrictEqual(null) const encoded = data.encode() - const hash = data.hash.toString('hex') - - const decrypted = await scrypt.decrypt(encoded, passphrase, hash) + const decrypted = await scrypt.decrypt(encoded, passphrase) expect(decrypted).toStrictEqual(buffer) expect(decrypted.toString('hex')).toStrictEqual(privKey) }) diff --git a/packages/jellyfish-wallet-encrypted/package.json b/packages/jellyfish-wallet-encrypted/package.json index 7d1b042adf..7a7632ee3b 100644 --- a/packages/jellyfish-wallet-encrypted/package.json +++ b/packages/jellyfish-wallet-encrypted/package.json @@ -35,8 +35,7 @@ "build": "tsc -b ./tsconfig.build.json" }, "dependencies": { - "@defichain/jellyfish-crypto": "0.0.0", - "@defichain/jellyfish-wallet": "0.0.0", + "@defichain/jellyfish-wallet-mnemonic": "0.0.0", "scryptsy": "^2.1.0" }, "devDependencies": { diff --git a/packages/jellyfish-wallet-encrypted/src/hd_node.ts b/packages/jellyfish-wallet-encrypted/src/hd_node.ts new file mode 100644 index 0000000000..9ed3a23767 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/src/hd_node.ts @@ -0,0 +1,124 @@ +import { WalletHdNodeProvider } from '@defichain/jellyfish-wallet' +import * as bip32 from 'bip32' +import { Bip32Options, MnemonicHdNode, MnemonicHdNodeProvider } from '@defichain/jellyfish-wallet-mnemonic' +import { Scrypt } from './scrypt' + +/** + * EncryptedMnemonicHdNode extends MnemonicHdNode to implement promise-based privKey resolution. + * This allows latent based implementation where privKey need to be decrypted. + * + * - BIP32 Hierarchical Deterministic Wallets + * - BIP39 Mnemonic code for generating deterministic keys + * - BIP44 Multi-Account Hierarchy for Deterministic Wallets + */ +export class EncryptedMnemonicHdNode extends MnemonicHdNode { + constructor ( + path: string, + chainCode: Buffer, + options: Bip32Options, + private readonly rootPubKey: Buffer, + private readonly promisePrivKey: () => Promise + ) { + super(path, Buffer.alloc(0), chainCode, options) + } + + /** + * Latent based implementation where privKey need to be resolved via a promise. + */ + protected async deriveNode (): Promise { + const rootPrivKey = await this.promisePrivKey() + return bip32.fromPrivateKey(rootPrivKey, this.chainCode, this.options) + .derivePath(this.path) + } + + /** + * @return Promise compressed public key + */ + async publicKey (): Promise { + return bip32.fromPublicKey(this.rootPubKey, this.chainCode, this.options) + .derivePath(this.path) + .publicKey + } +} + +/** + * EncryptedProviderData data encoded as hex. + */ +interface EncryptedProviderData { + /* Encoded as string hex */ + pubKey: string + /* Encoded as string hex */ + chainCode: string + /* Encoded as string hex */ + encryptedPrivKey: string +} + +/** + * Promise based Passphrase Prompt from EncryptedHdNodeProvider. + * For on-demand request passphrase to decrypt EncryptedProviderData. + */ +export type PromptPassphrase = () => Promise + +/** + * EncryptedHdNodeProvider implements MnemonicHdNode implementation privateKey on-demand decryption via scrypt. + * + */ +export class EncryptedHdNodeProvider implements WalletHdNodeProvider { + private constructor ( + private readonly data: EncryptedProviderData, + private readonly options: Bip32Options, + private readonly scrypt: Scrypt, + private readonly promptPassphrase: PromptPassphrase + ) { + } + + /** + * @param {string} path to derive with on-demand node + * @return EncryptedMnemonicHdNode with promisePrivKey that will only be resolved and decrypted when privateKey is accessed + */ + derive (path: string): EncryptedMnemonicHdNode { + const encrypted = this.data.encryptedPrivKey + const rootPubKey = Buffer.from(this.data.pubKey, 'hex') + const chainCode = Buffer.from(this.data.chainCode, 'hex') + + const promisePrivKey = async (): Promise => { + const passphrase = await this.promptPassphrase() + return this.scrypt.decrypt(encrypted, passphrase) + } + + return new EncryptedMnemonicHdNode(path, chainCode, this.options, rootPubKey, promisePrivKey) + } + + /** + * @param {string[]} words to convert into EncryptedProviderData + * @param {Bip32Options} options + * @param {string} scrypt to encrypt mnemonic words + * @param {string} passphrase to encrypt mnemonic words with + * @return EncryptedProviderData with unencrypted "pubKey & chainCode" and scrypt encoded 'encryptedPrivKey' + */ + static wordsToEncryptedData (words: string[], options: Bip32Options, scrypt: Scrypt, passphrase: string): EncryptedProviderData { + const mnemonic = MnemonicHdNodeProvider.wordsToData(words, options) + const privKey = Buffer.from(mnemonic.privKey, 'hex') + const chainCode = Buffer.from(mnemonic.chainCode, 'hex') + + const root = bip32.fromPrivateKey(privKey, chainCode, options) + const encrypted = scrypt.encrypt(privKey, passphrase) + + return { + pubKey: root.publicKey.toString('hex'), + chainCode: mnemonic.chainCode, + encryptedPrivKey: encrypted.encode() + } + } + + /** + * @param {EncryptedProviderData} data with unencrypted "pubKey & chainCode" and scrypt encoded 'encryptedPrivKey' + * @param {Bip32Options} options + * @param {string} scrypt to decrypt encrypted private key + * @param {PromptPassphrase} promptPassphrase for on-demand request passphrase to decrypt encrypted private key + * @return EncryptedHdNodeProvider + */ + static init (data: EncryptedProviderData, options: Bip32Options, scrypt: Scrypt, promptPassphrase: PromptPassphrase): EncryptedHdNodeProvider { + return new EncryptedHdNodeProvider(data, options, scrypt, promptPassphrase) + } +} diff --git a/packages/jellyfish-wallet-encrypted/src/index.ts b/packages/jellyfish-wallet-encrypted/src/index.ts index 7ca3891512..89b9cb02da 100644 --- a/packages/jellyfish-wallet-encrypted/src/index.ts +++ b/packages/jellyfish-wallet-encrypted/src/index.ts @@ -1,3 +1,4 @@ +export * from './hd_node' export * from './provider/scrypt_provider' export * from './provider/scrypt_simple' export * from './scrypt' diff --git a/packages/jellyfish-wallet-encrypted/src/scrypt.ts b/packages/jellyfish-wallet-encrypted/src/scrypt.ts index 66d60b0fdc..404e48ca3e 100644 --- a/packages/jellyfish-wallet-encrypted/src/scrypt.ts +++ b/packages/jellyfish-wallet-encrypted/src/scrypt.ts @@ -103,11 +103,10 @@ export class Scrypt { * * @param {string} encrypted to decrypt * @param {string} passphrase to decrypted data, utf8 string in normalization format C - * @param {string} hash for verification * @returns {Promise} null if no data found in storage * @throws Error InvalidPassphrase if passphrase is invalid (decrypted value has no matching hash) */ - decrypt (encrypted: string, passphrase: string, hash: string): Buffer { + decrypt (encrypted: string, passphrase: string): Buffer { const data = EncryptedData.decode(encrypted) const key = this.scryptProvider.passphraseToKey(passphrase, data.hash, 64) @@ -123,8 +122,8 @@ export class Scrypt { const decrypted = Buffer.from([...d1, ...d2]) const dataHash = dSHA256(decrypted).slice(0, 4) - if (dataHash.toString('hex') !== hash) { - throw new Error('InvalidPassphrase') + if (dataHash.toString('hex') !== data.hash.toString('hex')) { + throw new Error('invalid hash') } return decrypted diff --git a/packages/jellyfish-wallet-encrypted/tsconfig.build.json b/packages/jellyfish-wallet-encrypted/tsconfig.build.json index d4e0fc2696..88096275cd 100644 --- a/packages/jellyfish-wallet-encrypted/tsconfig.build.json +++ b/packages/jellyfish-wallet-encrypted/tsconfig.build.json @@ -8,8 +8,6 @@ "rootDir": "./src", }, "references": [ - {"path": "../jellyfish-crypto/tsconfig.build.json"}, - {"path": "../jellyfish-wallet/tsconfig.build.json"}, {"path": "../jellyfish-wallet-mnemonic/tsconfig.build.json"} ] } diff --git a/packages/jellyfish-wallet-mnemonic/__tests__/bip32.test.ts b/packages/jellyfish-wallet-mnemonic/__tests__/bip32.test.ts index 73a6d4e936..130c1656c2 100644 --- a/packages/jellyfish-wallet-mnemonic/__tests__/bip32.test.ts +++ b/packages/jellyfish-wallet-mnemonic/__tests__/bip32.test.ts @@ -98,6 +98,54 @@ describe('24 words: random', () => { expect(signed.witness[0].scripts[1].hex.length).toStrictEqual(66) }) }) + + describe('0/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('0/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.length).toStrictEqual(33) + }) + + it('should derive priv key', async () => { + const derivedPrivKey = await node.privateKey() + expect(derivedPrivKey.length).toStrictEqual(32) + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.length).toBeLessThanOrEqual(70) + expect(signature.length).toBeGreaterThanOrEqual(67) // 0.00001 probability of being this length + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex.length).toBeGreaterThanOrEqual(140) + expect(signed.witness[0].scripts[0].hex.length).toBeLessThanOrEqual(142) + expect(signed.witness[0].scripts[1].hex.length).toStrictEqual(66) + }) + }) }) describe('24 words: abandon x23 art', () => { @@ -201,4 +249,96 @@ describe('24 words: abandon x23 art', () => { expect(signed.witness[0].scripts[1].hex).toStrictEqual(pubKey) }) }) + + describe('0/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('0/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.toString('hex')).toStrictEqual('03d0d24a126c861c02622cb4ee75b860d6e65d1d6ffee20bf5793c1a00ade37db5') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('209fab36379401ac23960f0890ac1cc880e9c6e351a8525dac59a3ea6bb4ebb7') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('3044022007340e6a1052ca16acb180f33dd6781c60e2aa1acd27f4b50c13c6f370c9c8a802206fca4fd8263eb84d6e2c8d6111a5663b5da59eec843aab5cbfcbb273d497917c') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex).toStrictEqual('304402202c7ba3ded9cb503e8afb8a14ddb16ef4ea66043157e2a76e40d5d534fa80114302202aa1ea35654e2cd59a9f2e325037b38ef3d3939b47041782f05c819b1090dbd201') + expect(signed.witness[0].scripts[1].hex).toStrictEqual('03d0d24a126c861c02622cb4ee75b860d6e65d1d6ffee20bf5793c1a00ade37db5') + }) + }) + + describe('1/0/0', () => { + let node: MnemonicHdNode + + beforeEach(() => { + node = provider.derive('1/0/0') + }) + + it('should derive pub key', async () => { + const derivedPubKey = await node.publicKey() + expect(derivedPubKey.toString('hex')).toStrictEqual('0332b504dca50e2f9f369ac3bdc1a29fc5a082c9a35cc60f54d4c115518bb7a824') + }) + + it('should derive priv key', async () => { + const privKey = await node.privateKey() + expect(privKey.toString('hex')).toStrictEqual('12137509a9c70bc0ac55f0897577ba55cd1c222a209096f7b27f3de4c1eef30d') + }) + + it('should sign and verify', async () => { + const hash = Buffer.from('e9071e75e25b8a1e298a72f0d2e9f4f95a0f5cdf86a533cda597eb402ed13b3a', 'hex') + + const signature = await node.sign(hash) + expect(signature.toString('hex')).toStrictEqual('3044022036a7d5ad851541745fa4081327ef846c262d46640cbb1f5d989c07db3400fe4a0220384de187749c731ea47036d4f4bb77db69961359b8c70a1da5750b9acec2e573') + + const valid = await node.verify(hash, signature) + expect(valid).toStrictEqual(true) + }) + + it('should sign tx', async () => { + const signed = await node.signTx(transaction, [{ + ...prevout, + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(await node.publicKey()), 'little') + ] + } + }]) + + expect(signed.witness.length).toStrictEqual(1) + expect(signed.witness[0].scripts.length).toStrictEqual(2) + + expect(signed.witness[0].scripts[0].hex).toStrictEqual('30440220729650b5ab2e325e13da49e474fa5f434f7c68f2d62a734e48c982a921b38830022069c02966ede1db87ac726326da7d5467775de23ec56516f151b31abbb90ec7e701') + expect(signed.witness[0].scripts[1].hex).toStrictEqual('0332b504dca50e2f9f369ac3bdc1a29fc5a082c9a35cc60f54d4c115518bb7a824') + }) + }) }) diff --git a/packages/jellyfish-wallet/README.md b/packages/jellyfish-wallet/README.md index e1cfff8c31..7fc1d639c9 100644 --- a/packages/jellyfish-wallet/README.md +++ b/packages/jellyfish-wallet/README.md @@ -7,8 +7,9 @@ > This is created for better UX, your daily average users. Jellyfish wallet is a managed wallet, where account can get discovered from an HD seed. Accounts in jellyfish-wallet, -has only one address for simplicity. Accounts path are derived from seed with path: `44'/1129'/{ACCOUNT}/0/0`. It uses a -provider model where the node and account is agnostic and provided on demand to the managed wallet. +has only one address for simplicity. Accounts path are derived from seed with path: `{ACCOUNT}/0/0`. Non-hardened path +is used to allow encrypted wallet implementation where only xpubkey is required. It uses a provider model where the node +and account is agnostic and provided on demand to the managed wallet. Being a managed wallet design it uses must use conventional defaults and options must be kept to none. Address must stay consistent hence `bech32` must be used and, etc. diff --git a/packages/jellyfish-wallet/__tests__/wallet.test.ts b/packages/jellyfish-wallet/__tests__/wallet.test.ts index 4aa9904ed0..b92055171f 100644 --- a/packages/jellyfish-wallet/__tests__/wallet.test.ts +++ b/packages/jellyfish-wallet/__tests__/wallet.test.ts @@ -15,7 +15,7 @@ describe('discover accounts', () => { it('should discover [0] when [0] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -25,7 +25,7 @@ describe('discover accounts', () => { it('should discover [] when [1] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24' + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -35,8 +35,8 @@ describe('discover accounts', () => { it('should discover [0,1] when [0,1] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8', + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -46,9 +46,9 @@ describe('discover accounts', () => { it('should discover [0,1,2] when [0,1,2] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24', - 'bcrt1qrvt6c60848p8y8vd3pejdt33davp5ka9vxupuj' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8', + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38', + 'bcrt1q2v5wa73qe0ychdheppaeae5s594u3up2d0tha8' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -58,10 +58,10 @@ describe('discover accounts', () => { it('should discover [0,1] when [0,1,3,4] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24', - 'bcrt1qur2tmednr6e52u9du972nqvua60egwqkf98ps8', - 'bcrt1qxvvp3tz5u8t90nwwjzsalha66zk9em95tgn3fk' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8', + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38', + 'bcrt1q9a2mrm2sp4xpu5k33jakr4jk9zqcwtf3ns3h7j', + 'bcrt1qh7safzu8d2p02vlnhsm2995t3der3qk75wl8y6' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -71,8 +71,8 @@ describe('discover accounts', () => { it('should discover [0] when [0,1] has activity as max account is set to 1', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8', + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) @@ -91,7 +91,7 @@ describe('is usable', () => { it('[0,1] should be usable when [0] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) expect(await wallet.isUsable(0)).toStrictEqual(true) @@ -100,8 +100,8 @@ describe('is usable', () => { it('[0,1,2] should be usable when [0,1] has activity', async () => { const accountProvider = new TestAccountProvider([ - 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', - 'bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24' + 'bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8', + 'bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38' ]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) expect(await wallet.isUsable(0)).toStrictEqual(true) @@ -109,7 +109,7 @@ describe('is usable', () => { expect(await wallet.isUsable(2)).toStrictEqual(true) }) - it('[2] should be usable when [0,1] has no activity', async () => { + it('[2] should not be usable when [0,1] has no activity', async () => { const accountProvider = new TestAccountProvider([]) const wallet = new JellyfishWallet(nodeProvider, accountProvider) expect(await wallet.isUsable(2)).toStrictEqual(false) @@ -123,30 +123,54 @@ describe('get accounts', () => { it('should get account 0', async () => { const account = wallet.get(0) const address = await account.getAddress() - expect(address).toStrictEqual('bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r') + expect(address).toStrictEqual('bcrt1qtxqjthltev9zqzfqkgt3t758zmdq2twhf2hkj8') }) it('should get account 1', async () => { const account = wallet.get(1) const address = await account.getAddress() - expect(address).toStrictEqual('bcrt1qnkmmcu79glheaqsq3gj4gg4675z3cjzn39dt24') + expect(address).toStrictEqual('bcrt1qncjrhlntyyrv6dk5xjn0ep6sjfrqv478365v38') }) it('should get account 2', async () => { const account = wallet.get(2) const address = await account.getAddress() - expect(address).toStrictEqual('bcrt1qrvt6c60848p8y8vd3pejdt33davp5ka9vxupuj') + expect(address).toStrictEqual('bcrt1q2v5wa73qe0ychdheppaeae5s594u3up2d0tha8') }) it('should get account 3', async () => { const account = wallet.get(3) const address = await account.getAddress() - expect(address).toStrictEqual('bcrt1qur2tmednr6e52u9du972nqvua60egwqkf98ps8') + expect(address).toStrictEqual('bcrt1q9a2mrm2sp4xpu5k33jakr4jk9zqcwtf3ns3h7j') }) it('should get account 4', async () => { - const account = wallet.get(4) - const address = await account.getAddress() - expect(address).toStrictEqual('bcrt1qxvvp3tz5u8t90nwwjzsalha66zk9em95tgn3fk') + const address = await wallet.get(4).getAddress() + expect(address).toStrictEqual('bcrt1qh7safzu8d2p02vlnhsm2995t3der3qk75wl8y6') + }) + + it('should get account 5', async () => { + const address = await wallet.get(5).getAddress() + expect(address).toStrictEqual('bcrt1qep3yprgx6q0pwuwat3hqj4fnkd3rfkd6cqev6j') + }) + + it('should get account 6', async () => { + const address = await wallet.get(6).getAddress() + expect(address).toStrictEqual('bcrt1qqgwks52gufe3e2jplg6k40cat5prgtlr6f2p36') + }) + + it('should get account 7', async () => { + const address = await wallet.get(7).getAddress() + expect(address).toStrictEqual('bcrt1q65yacygafrchwxv90wfc2jzdw6c4sl8ejn0mdf') + }) + + it('should get account 8', async () => { + const address = await wallet.get(8).getAddress() + expect(address).toStrictEqual('bcrt1qkt7rvkzk8qs7rk54vghrtzcdxfqazscmmp30hk') + }) + + it('should get account 9', async () => { + const address = await wallet.get(9).getAddress() + expect(address).toStrictEqual('bcrt1qgpu5k3v66qjf8lc4p4lny0uwdxv6vf94axnjkf') }) }) diff --git a/packages/jellyfish-wallet/src/wallet.ts b/packages/jellyfish-wallet/src/wallet.ts index 449c6b4f99..8ace772c4a 100644 --- a/packages/jellyfish-wallet/src/wallet.ts +++ b/packages/jellyfish-wallet/src/wallet.ts @@ -1,11 +1,6 @@ import { WalletAccount, WalletAccountProvider } from './wallet_account' import { WalletHdNode, WalletHdNodeProvider } from './wallet_hd_node' -/** - * DFI CoinType - */ -const COIN_TYPE: number = 1129 - /** * Jellyfish managed wallet. * WalletHdNode instance is provided by WalletHdNodeProvider. @@ -29,7 +24,7 @@ export class JellyfishWallet */ get (account: number): Account { - const path = `44'/${COIN_TYPE}'/${account}'/0/0` + const path = `${account}/0/0` const node = this.nodeProvider.derive(path) return this.accountProvider.provide(node) } diff --git a/packages/jellyfish-wallet/src/wallet_account.ts b/packages/jellyfish-wallet/src/wallet_account.ts index 4bff0b2062..8d817d7420 100644 --- a/packages/jellyfish-wallet/src/wallet_account.ts +++ b/packages/jellyfish-wallet/src/wallet_account.ts @@ -7,7 +7,7 @@ import { DeFiAddress } from '@defichain/jellyfish-address' /** * An HDW is organized as several 'accounts'. * Accounts are numbered, the default account ("") being number 0. - * Account are derived from root and the pubkey to be used is `44'/1129'/${account}'/0/0` + * Account are derived from root and the pubkey to be used is `${account}/0/0` * * WalletAccount implementation uses NATIVE SEGWIT redeem script exclusively. */