Skip to content

Commit

Permalink
Added SPV refundHtlc RPC (#529)
Browse files Browse the repository at this point in the history
* Added SPV refundHtlc RPC

* fix typo

* Add comments about refundHtlc condition
  • Loading branch information
Jouzo authored Aug 16, 2021
1 parent d6b69e5 commit 553e8f7
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 5 deletions.
104 changes: 104 additions & 0 deletions packages/jellyfish-api-core/__tests__/category/spv/refundHtlc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { BigNumber, RpcApiError } from '@defichain/jellyfish-api-core'
import { ContainerAdapterClient } from '../../container_adapter_client'

describe('Spv', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

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

await container.spv.fundAddress(await container.call('spv_getnewaddress')) // Funds 1 BTC
})

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

it('should refundHtlc', async () => {
const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

await container.call('spv_sendtoaddress', [htlc.address, 0.1]) // Funds HTLC address

const destinationAddress = await container.call('spv_getnewaddress')
const result = await client.spv.refundHtlc(htlc.address, destinationAddress) // This refund should only happen after timeout threshold set in createHtlc. See https://en.bitcoin.it/wiki/BIP_0199
expect(typeof result.txid).toStrictEqual('string')
expect(result.txid.length).toStrictEqual(64)
expect(result.sendmessage).toStrictEqual('Success')

/**
* Assert that the destination address received the refund
*/
const listReceivingAddresses = await container.call('spv_listreceivedbyaddress')
const receivingAddress = listReceivingAddresses.find(({ address }: { address: string }) => address === destinationAddress)
expect(receivingAddress.address).toStrictEqual(destinationAddress)
expect(receivingAddress.txids.some((txid: string) => txid === result.txid)).toStrictEqual(true)
})

it('should refundHtlc with custom feeRate', async () => {
const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

await container.call('spv_sendtoaddress', [htlc.address, 0.1]) // Funds HTLC address

const destinationAddress = await container.call('spv_getnewaddress')
const result = await client.spv.refundHtlc(htlc.address, destinationAddress, { feeRate: new BigNumber('20000') }) // This refund should only happen after timeout threshold set in createHtlc. See https://en.bitcoin.it/wiki/BIP_0199
expect(typeof result.txid).toStrictEqual('string')
expect(result.txid.length).toStrictEqual(64)
expect(result.sendmessage).toStrictEqual('Success')

/**
* Assert that the destination address received the refund
*/
const listReceivingAddresses = await container.call('spv_listreceivedbyaddress')
const receivingAddress = listReceivingAddresses.find(({ address }: { address: string }) => address === destinationAddress)
expect(receivingAddress.address).toStrictEqual(destinationAddress)
expect(receivingAddress.txids.some((txid: string) => txid === result.txid)).toStrictEqual(true)
})

it('should not refundHtlc when no unspent HTLC outputs found', async () => {
const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

const promise = client.spv.refundHtlc(htlc.address, await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow("RpcApiError: 'No unspent HTLC outputs found', code: -4, method: spv_refundhtlc")
})

it('should not refundHtlc with invalid HTLC address', async () => {
const promise = client.spv.refundHtlc('XXXX', await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow("RpcApiError: 'Invalid address', code: -5, method: spv_refundhtlc")
})

it('should not refundHtlc with invalid destination address', async () => {
const promise = client.spv.refundHtlc(await container.call('spv_getnewaddress'), 'XXXX')
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow("RpcApiError: 'Invalid destination address', code: -5, method: spv_refundhtlc")
})

it('should not refundHtlc with not enough funds to cover fee', async () => {
const pubKeyA = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const pubKeyB = await container.call('spv_getaddresspubkey', [await container.call('spv_getnewaddress')])
const htlc = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

await container.call('spv_sendtoaddress', [htlc.address, 0.00000546]) // Funds HTLC address with dust

const promise = client.spv.refundHtlc(htlc.address, await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow("RpcApiError: 'Not enough funds to cover fee', code: -1, method: spv_refundhtlc")
})

it('should not refundHtlc when redeem script not found in wallet', async () => {
const randomAddress = '2Mu4edSkC5gKVwYayfDq2fTFwT6YD4mujSX'
const promise = client.spv.refundHtlc(randomAddress, await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow("RpcApiError: 'Redeem script not found in wallet', code: -4, method: spv_refundhtlc")
})
})
21 changes: 18 additions & 3 deletions packages/jellyfish-api-core/src/category/spv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ export class Spv {
*
* @param {string} address Bitcoin address
* @param {BigNumber} amount Bitcoin amount
* @param {SendToAddressOptions} [options]
* @param {SpvDefaultOptions} [options]
* @param {BigNumber} [options.feeRate=10000] Fee rate in satoshis per KB. Minimum is 1000.
* @return {Promise<SendMessageResult>}
*/
async sendToAddress (address: string, amount: BigNumber, options: SendToAddressOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult> {
async sendToAddress (address: string, amount: BigNumber, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult> {
return await this.client.call('spv_sendtoaddress', [address, amount, options.feeRate], 'bignumber')
}

Expand Down Expand Up @@ -105,6 +105,20 @@ export class Spv {
async getHtlcSeed (address: string): Promise<string> {
return await this.client.call('spv_gethtlcseed', [address], 'number')
}

/**
* Refunds all coins in HTLC address.
* Can be used after the timeout threshold set in createHtlc. See https://en.bitcoin.it/wiki/BIP_0199
*
* @param {string} scriptAddress HTLC address
* @param {string} destinationAddress Destination for funds in the HTLC
* @param {SpvDefaultOptions} [options]
* @param {BigNumber} [options.feeRate=10000] Fee rate in satoshis per KB. Minimum is 1000.
* @return {Promise<SendMessageResult>}
*/
async refundHtlc (scriptAddress: string, destinationAddress: string, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult> {
return await this.client.call('spv_refundhtlc', [scriptAddress, destinationAddress, options.feeRate], 'number')
}
}

export interface ReceivedByAddressInfo {
Expand All @@ -120,7 +134,8 @@ export interface ReceivedByAddressInfo {
txids: string[]
}

export interface SendToAddressOptions {
export interface SpvDefaultOptions {
/** Fee rate in satoshis per KB */
feeRate?: BigNumber
}

Expand Down
23 changes: 21 additions & 2 deletions website/docs/jellyfish/api/spv.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ Send a Bitcoin amount to a given address.

```ts title="client.spv.sendToAddress()"
interface spv {
sendToAddress (address: string, amount: BigNumber, options: SendToAddressOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult>
sendToAddress (address: string, amount: BigNumber, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult>
}

interface SendToAddressOptions {
interface SpvDefaultOptions {
feeRate?: BigNumber
}

Expand Down Expand Up @@ -138,3 +138,22 @@ interface spv {
getHtlcSeed (address: string): Promise<string>
}
```

## refundHtlc

Refunds all coins in HTLC address.

```ts title="client.spv.refundHtlc()"
interface spv {
refundHtlc (scriptAddress: string, destinationAddress: string, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise<SendMessageResult>
}

interface SpvDefaultOptions {
feeRate?: BigNumber
}

interface SendMessageResult {
txid: string
sendmessage: string
}
```

0 comments on commit 553e8f7

Please sign in to comment.