Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jellyfish-api-core): add encryptWallet, walletPassphrase, walletPassphraseChange, walletLock RPC #1969

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/node/CATEGORIES/05-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,49 @@ interface wallet {
signMessage (address: string, message: string): Promise<string>
}
```

## encryptWallet

Encrypts the wallet for the first time using a custom ‘passphrase’. Transactions related to private keys will thereafter require a passphrase before execution.

To unlock the wallet, use [walletPassphrase](#walletPassphrase)

```ts title="client.wallet.encryptWallet()"
interface wallet {
encryptWallet (passphrase: string): Promise<string>
}
```

## walletPassphrase

Stores the wallet decryption key in memory for ‘timeout’ seconds. Calling `walletPassphrase` when wallet is unlocked will set a new unlock time that overrides the old setting.

To encrypt the wallet for the first time, use [encryptWallet](#encryptWallet)

```ts title="client.wallet.walletPassphrase()"
interface wallet {
walletPassphrase (passphrase: string, timeout: number): Promise<void>
}
```

## walletPassphraseChange

Changes the wallet passphrase from ‘oldpassphrase’ to ‘newpassphrase’.

```ts title="client.wallet.walletPassphraseChange()"
interface wallet {
walletPassphraseChange (oldpassphrase: string, newpassphrase: string): Promise<void>
}
```

## walletLock

Removes the wallet encryption key from memory, locking the wallet.

After locking the wallet, `walletPassphrase` must be called again to use methods that requires an unlocked wallet.

```ts title="client.wallet.walletLock()"
interface wallet {
walletLock (): Promise<void>
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { MasterNodeRegTestContainer, RegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { RpcApiError } from '@defichain/jellyfish-api-core'

describe('Wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
})

afterAll(async () => {
await container.stop()
})

it('should throw error if passphrase for encryptWallet is empty', async () => {
const promise = client.wallet.encryptWallet('')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'passphrase can not be empty', code: -8, method: encryptwallet"
)
})

it('should encryptWallet with a given passphrase', async () => {
const promise = await client.wallet.encryptWallet('yourpassphrase')

expect(promise).toStrictEqual(
'wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup.'
)
})

it('should throw error when encryptWallet is called again after encryption', async () => {
const promise = client.wallet.encryptWallet('yourpassphrase')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: running with an encrypted wallet, but encryptwallet was called.', code: -15, method: encryptwallet"
)
})
})

describe('Wallet without masternode', () => {
const container = new RegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
})

afterAll(async () => {
await container.stop()
})

it('should throw error if passphrase for wallet encryption is empty', async () => {
const promise = client.wallet.encryptWallet('')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'passphrase can not be empty', code: -8, method: encryptwallet"
)
})

it('should encryptWallet with a given passphrase', async () => {
const promise = await client.wallet.encryptWallet('yourpassphrase')

expect(promise).toStrictEqual(
'wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup.'
)
})

it('should throw error when encryptWallet is called again after encryption', async () => {
const promise = client.wallet.encryptWallet('yourpassphrase')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: running with an encrypted wallet, but encryptwallet was called.', code: -15, method: encryptwallet"
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { RpcApiError } from '@defichain/jellyfish-api-core'

describe('Unencrypted wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
})

afterAll(async () => {
await container.stop()
})

it('should throw error when walletLock is called', async () => {
const promise = client.wallet.walletLock()

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: running with an unencrypted wallet, but walletlock was called.', code: -15, method: walletlock"
)
})
})

describe('Encrypted wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await client.wallet.encryptWallet('password')
await client.wallet.walletPassphrase('password', 10000)
})

afterAll(async () => {
await container.stop()
})

it('should walletLock without failing', async () => {
const method = client.wallet.importPrivKey(await client.wallet.dumpPrivKey(await client.wallet.getNewAddress()))
const promise = client.wallet.walletLock()

await expect(method).resolves.not.toThrow()
await expect(promise).resolves.not.toThrow()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { RpcApiError } from '@defichain/jellyfish-api-core'

describe('Unencrypted wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
})

afterAll(async () => {
await container.stop()
})

it('should throw error when walletPassphrase is called', async () => {
const promise = client.wallet.walletPassphrase('password', -100)

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: running with an unencrypted wallet, but walletpassphrase was called.', code: -15, method: walletpassphrase"
)
})
})

describe('Encrypted wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await client.wallet.encryptWallet('password')
})

afterAll(async () => {
await container.stop()
})

it('should throw error when walletPassphrase is called with a negative timeout', async () => {
const promise = client.wallet.walletPassphrase('password', -100)

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Timeout cannot be negative.', code: -8, method: walletpassphrase"
)
})

it('should throw error when walletPassphrase is called without a passphrase', async () => {
const promise = client.wallet.walletPassphrase('', 100)

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'passphrase can not be empty', code: -8, method: walletpassphrase"
)
})

it('should throw error when walletPassphrase is called with a wrong passphrase', async () => {
const promise = client.wallet.walletPassphrase('incorrectpassword', 100)

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: The wallet passphrase entered was incorrect.', code: -14, method: walletpassphrase"
)
})

it('should unlock wallet when walletPassphrase is called with the correct passphrase', async () => {
const promise = client.wallet.walletPassphrase('password', 100)

await expect(promise).resolves.not.toThrow()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { RpcApiError } from '@defichain/jellyfish-api-core'

describe('Unencrypted Wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
})

afterAll(async () => {
await container.stop()
})

it('should throw error when walletPassphraseChange is called', async () => {
const promise = client.wallet.walletPassphraseChange('wrongpassword', 'newpassword')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: running with an unencrypted wallet, but walletpassphrasechange was called.', code: -15, method: walletpassphrasechange"
)
})
})

describe('Encrypted Wallet on masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await client.wallet.encryptWallet('password')
})

afterAll(async () => {
await container.stop()
})

it('should throw error when walletPassphraseChange is called with an incorrect passphrase', async () => {
const promise = client.wallet.walletPassphraseChange('wrongpassword', 'newpassword')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'Error: The wallet passphrase entered was incorrect.', code: -14, method: walletpassphrasechange"
)
})

it('should throw error when walletPassphraseChange is called with an empty passphrase', async () => {
const promise = client.wallet.walletPassphraseChange('', 'newpassword')

await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow(
"RpcApiError: 'passphrase can not be empty', code: -8, method: walletpassphrasechange"
)
})

it('should walletPassphraseChange when the correct old passphrase is provided', async () => {
const promise = client.wallet.walletPassphraseChange('password', 'newpassword')

await expect(promise).resolves.not.toThrow()
})
})
45 changes: 45 additions & 0 deletions packages/jellyfish-api-core/src/category/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,51 @@ export class Wallet {
async signMessage (address: string, message: string): Promise<string> {
return await this.client.call('signmessage', [address, message], 'number')
}

/**
* Encrypts the wallet for the first time using a custom ‘passphrase’.
* Transactions related to private keys will thereafter require a passphrase before execution.
* To unlock wallet, use 'walletpassphrase'
*
* @param {string} passphrase The wallet passphrase. Must be at least 1 character, but should be long.
* @return {Promise<string>}
*/
async encryptWallet (passphrase: string): Promise<string> {
return await this.client.call('encryptwallet', [passphrase], 'number')
}

/**
* Stores the wallet decryption key in memory for ‘timeout’ seconds.
* Calling 'walletpassphrase' when wallet is unlocked will set a new unlock time that overrides the old setting.
*
* @param {string} passphrase The wallet passphrase. Must be at least 1 character, but should be long.
* @param {number} timeout The time to keep the decryption key in seconds; capped at 100000000 (~3 years).
* @return {Promise<>}
*/
async walletPassphrase (passphrase: string, timeout: number): Promise<void> {
return await this.client.call('walletpassphrase', [passphrase, timeout], 'number')
}

/**
* Changes the wallet passphrase from ‘oldpassphrase’ to ‘newpassphrase’.
*
* @param {string} oldpassphrase The old wallet passphrase.
* @param {string} newpassphrase The new wallet passphrase.
* @return {Promise<>}
*/
async walletPassphraseChange (oldpassphrase: string, newpassphrase: string): Promise<void> {
return await this.client.call('walletpassphrasechange', [oldpassphrase, newpassphrase], 'number')
}

/**
* Removes the wallet encryption key from memory, locking the wallet.
* Unlock wallet by calling 'walletpassphrase' to perform wallet-related methods.
*
* @return {Promise<>}
*/
async walletLock (): Promise<void> {
return await this.client.call('walletlock', [], 'number')
}
}

export interface UTXO {
Expand Down