Skip to content

Commit

Permalink
feat(jellyfish-api-core): add signMessage RPC (#1934)
Browse files Browse the repository at this point in the history
**What this PR does / why we need it:** 
/kind feature

**Which issue(s) does this PR fixes?:** 
Adding `signMessage()` from
[Issue#48](#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)
  • Loading branch information
shancht authored Jan 11, 2023
1 parent 6f073ee commit 654f382
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/node/CATEGORIES/05-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,13 @@ interface wallet {
listWallets (): Promise<string[]>
}
```

## 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<string>
}
```
Original file line number Diff line number Diff line change
@@ -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.'
}
})
})
})
12 changes: 12 additions & 0 deletions packages/jellyfish-api-core/src/category/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ export class Wallet {
async listWallets (): Promise<string[]> {
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<string>}
*/
async signMessage (address: string, message: string): Promise<string> {
return await this.client.call('signmessage', [address, message], 'number')
}
}

export interface UTXO {
Expand Down

0 comments on commit 654f382

Please sign in to comment.