From 553e8f7b1c7379ef7c71c47907078a26bd936113 Mon Sep 17 00:00:00 2001 From: Jouzo Date: Mon, 16 Aug 2021 06:30:23 +0200 Subject: [PATCH] Added SPV refundHtlc RPC (#529) * Added SPV refundHtlc RPC * fix typo * Add comments about refundHtlc condition --- .../__tests__/category/spv/refundHtlc.test.ts | 104 ++++++++++++++++++ .../jellyfish-api-core/src/category/spv.ts | 21 +++- website/docs/jellyfish/api/spv.md | 23 +++- 3 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/spv/refundHtlc.test.ts diff --git a/packages/jellyfish-api-core/__tests__/category/spv/refundHtlc.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/refundHtlc.test.ts new file mode 100644 index 0000000000..843c5bce96 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/refundHtlc.test.ts @@ -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") + }) +}) diff --git a/packages/jellyfish-api-core/src/category/spv.ts b/packages/jellyfish-api-core/src/category/spv.ts index af51cb3ee4..c2270f2aeb 100644 --- a/packages/jellyfish-api-core/src/category/spv.ts +++ b/packages/jellyfish-api-core/src/category/spv.ts @@ -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} */ - async sendToAddress (address: string, amount: BigNumber, options: SendToAddressOptions = { feeRate: new BigNumber('10000') }): Promise { + async sendToAddress (address: string, amount: BigNumber, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise { return await this.client.call('spv_sendtoaddress', [address, amount, options.feeRate], 'bignumber') } @@ -105,6 +105,20 @@ export class Spv { async getHtlcSeed (address: string): Promise { 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} + */ + async refundHtlc (scriptAddress: string, destinationAddress: string, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise { + return await this.client.call('spv_refundhtlc', [scriptAddress, destinationAddress, options.feeRate], 'number') + } } export interface ReceivedByAddressInfo { @@ -120,7 +134,8 @@ export interface ReceivedByAddressInfo { txids: string[] } -export interface SendToAddressOptions { +export interface SpvDefaultOptions { + /** Fee rate in satoshis per KB */ feeRate?: BigNumber } diff --git a/website/docs/jellyfish/api/spv.md b/website/docs/jellyfish/api/spv.md index 854b54f47f..20ab802e43 100644 --- a/website/docs/jellyfish/api/spv.md +++ b/website/docs/jellyfish/api/spv.md @@ -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 + sendToAddress (address: string, amount: BigNumber, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise } -interface SendToAddressOptions { +interface SpvDefaultOptions { feeRate?: BigNumber } @@ -138,3 +138,22 @@ interface spv { getHtlcSeed (address: string): Promise } ``` + +## 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 +} + +interface SpvDefaultOptions { + feeRate?: BigNumber +} + +interface SendMessageResult { + txid: string + sendmessage: string +} +```