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.
*/