From 654f3822158dfe6e86184d11eeec10fa3b2f7e6a Mon Sep 17 00:00:00 2001 From: Shana <45248357+shancht@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:12:49 +0800 Subject: [PATCH] feat(jellyfish-api-core): add signMessage RPC (#1934) **What this PR does / why we need it:** /kind feature **Which issue(s) does this PR fixes?:** Adding `signMessage()` from [Issue#48](https://github.com/JellyfishSDK/jellyfish/issues/48) This features allow users to sign an arbitrary message using the private key of an address. It requires wallet passphrase to be set with `walletpassphrase` call if the wallet is encrypted. **Additional comments?:** The primary purpose of `signMessage()` is to present a signature to showcase that the funds are in the control of the private key holder. The holder may also generate a signature with custom prefixes to showcase proof of authorisation. Usage Example: [Here](https://bitcoin.stackexchange.com/a/3339/160) --- docs/node/CATEGORIES/05-wallet.md | 10 ++ .../category/wallet/signMessage.test.ts | 124 ++++++++++++++++++ .../jellyfish-api-core/src/category/wallet.ts | 12 ++ 3 files changed, 146 insertions(+) create mode 100644 packages/jellyfish-api-core/__tests__/category/wallet/signMessage.test.ts diff --git a/docs/node/CATEGORIES/05-wallet.md b/docs/node/CATEGORIES/05-wallet.md index f75762a2d5..6301aabb02 100644 --- a/docs/node/CATEGORIES/05-wallet.md +++ b/docs/node/CATEGORIES/05-wallet.md @@ -397,3 +397,13 @@ interface wallet { listWallets (): Promise } ``` + +## signMessage + +Sign a message with the private key of an address. Requires wallet to be unlocked for usage. Use `walletpassphrase` to unlock wallet. + +```ts title="client.wallet.signMessage()" +interface wallet { + signMessage (address: string, message: string): Promise +} +``` diff --git a/packages/jellyfish-api-core/__tests__/category/wallet/signMessage.test.ts b/packages/jellyfish-api-core/__tests__/category/wallet/signMessage.test.ts new file mode 100644 index 0000000000..31b7617c85 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/wallet/signMessage.test.ts @@ -0,0 +1,124 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { ContainerAdapterClient } from '../../container_adapter_client' +import { RpcApiError } from '@defichain/jellyfish-api-core' +import { wallet } from '../../../src' + +describe('Sign Message on masternode by unlocking encrpyted wallet', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await client.call('encryptwallet', ['password'], 'number') + await client.call('walletpassphrase', ['password', 10000], 'number') + }) + + afterAll(async () => { + await container.stop() + }) + + it('should throw error if BECH32 address is provided', async () => { + // getNewAddress() generates a BECH32 address by default + // signMessage() is not compatible with BECH32 address + const address = await client.wallet.getNewAddress() + const message = 'This is a test message' + + const promise = client.wallet.signMessage(address, message) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -3, + message: 'Address does not refer to key', + method: 'signmessage' + } + }) + }) + + it('should throw error if P2SH address is provided', async () => { + // signMessage() is not compatible with P2SH address + const address = await client.wallet.getNewAddress('', wallet.AddressType.P2SH_SEGWIT) + const message = 'This is a test message' + + const promise = client.wallet.signMessage(address, message) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -3, + message: 'Address does not refer to key', + method: 'signmessage' + } + }) + }) + + it('should throw error if invalid/no address is provided', async () => { + const message = 'This is a test message' + const promise = client.wallet.signMessage('', message) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -3, + message: 'Invalid address', + method: 'signmessage' + } + }) + }) + + it('should throw error if address provided does not contain private key', async () => { + const message = 'This is a test message' + const address = 'mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB' + const promise = client.wallet.signMessage(address, message) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -4, + message: 'Private key not available', + method: 'signmessage' + } + }) + }) + + it('should be verifiable using verifyMessage()', async () => { + // signMessage() is compatible with LEGACY address + const address = await client.wallet.getNewAddress('', wallet.AddressType.LEGACY) + const message = 'This is a test message' + + const signature = await client.wallet.signMessage(address, message) + + const verify = await client.call('verifymessage', [address, signature, message], 'number') + expect(verify).toStrictEqual(true) + }) +}) + +describe('Sign Message on masternode without unlocking encrypted wallet', () => { + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await client.call('encryptwallet', ['password'], 'number') + }) + + afterAll(async () => { + await container.stop() + }) + + it('should throw error even with compatible address', async () => { + // signMessage() is compatible with LEGACY address + const address = await client.wallet.getNewAddress('LEGACY example', wallet.AddressType.LEGACY) + const message = 'This is a test message' + + const promise = client.wallet.signMessage(address, message) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -13, + message: 'Error: Please enter the wallet passphrase with walletpassphrase first.' + } + }) + }) +}) diff --git a/packages/jellyfish-api-core/src/category/wallet.ts b/packages/jellyfish-api-core/src/category/wallet.ts index dc76a66d0a..8785b1796d 100644 --- a/packages/jellyfish-api-core/src/category/wallet.ts +++ b/packages/jellyfish-api-core/src/category/wallet.ts @@ -324,6 +324,18 @@ export class Wallet { async listWallets (): Promise { return await this.client.call('listwallets', [], 'number') } + + /** + * Sign a message with the private key of an address + * Requires wallet to be unlocked for usage. Use `walletpassphrase` to unlock wallet. + * + * @param {string} address The DeFi address to use for the private key. + * @param {string} message The message to create a signature of. + * @return {Promise} + */ + async signMessage (address: string, message: string): Promise { + return await this.client.call('signmessage', [address, message], 'number') + } } export interface UTXO {