diff --git a/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts b/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts new file mode 100644 index 0000000000..7cd210c14e --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/spv/claimHtlc.test.ts @@ -0,0 +1,135 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { 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.call('spv_fundaddress', [await container.call('spv_getnewaddress')]) // Funds 1 BTC + }) + + afterAll(async () => { + await container.stop() + }) + + it('should claimHtlc', 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 claimedHtlc = await client.spv.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + expect(typeof claimedHtlc.txid).toStrictEqual('string') + expect(claimedHtlc.sendmessage).toStrictEqual('Success') + }) + + it('should not claimHtlc 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.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'No unspent HTLC outputs found', code: -4, method: spv_claimhtlc") + }) + + it('should not claimHtlc when provided seed is not in hex form', 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.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: 'XXXX' } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Provided seed is not in hex form', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc when seed provided does not match seed hash in contract', 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.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: '00' } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Seed provided does not match seed hash in contract', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc with invalid address', 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.claimHtlc( + 'XXXX', + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid address', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc when redeem script not found in wallet', 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 randomAddress = '2Mu4edSkC5gKVwYayfDq2fTFwT6YD4mujSX' + + const promise = client.spv.claimHtlc( + randomAddress, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Redeem script not found in wallet', code: -4, method: spv_claimhtlc") + }) + + it('should not claimHtlc with invalid destination address', 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.claimHtlc( + htlc.address, + 'XXXX', + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Invalid destination address', code: -5, method: spv_claimhtlc") + }) + + it('should not claimHtlc 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.claimHtlc( + htlc.address, + await container.call('spv_getnewaddress'), + { seed: htlc.seed } + ) + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toThrow("RpcApiError: 'Not enough funds to cover fee', code: -1, method: spv_claimhtlc") + }) +}) diff --git a/packages/jellyfish-api-core/src/category/spv.ts b/packages/jellyfish-api-core/src/category/spv.ts index 05b4d31424..47599cba8e 100644 --- a/packages/jellyfish-api-core/src/category/spv.ts +++ b/packages/jellyfish-api-core/src/category/spv.ts @@ -81,6 +81,20 @@ export class Spv { async decodeHtlcScript (redeemScript: string): Promise { return await this.client.call('spv_decodehtlcscript', [redeemScript], 'number') } + + /** + * Claims all coins in HTLC address. + * + * @param {string} scriptAddress HTLC address + * @param {string} destinationAddress Destination address to send HTLC funds to + * @param {ClaimHtlcOptions} options + * @param {string} options.seed HTLC seed + * @param {BigNumber} [options.feeRate=10000] Fee rate in satoshis per KB. Minimum is 1000. + * @return {Promise} + */ + async claimHtlc (scriptAddress: string, destinationAddress: string, options: ClaimHtlcOptions): Promise { + return await this.client.call('spv_claimhtlc', [scriptAddress, destinationAddress, options.seed, options.feeRate], 'bignumber') + } } export interface ReceivedByAddressInfo { @@ -133,3 +147,10 @@ export interface DecodeHtlcResult { /** Hex-encoded seed hash */ hash: string } + +export interface ClaimHtlcOptions { + /** HTLC seed */ + seed: string + /** 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 7f49856273..1d65c04ae0 100644 --- a/website/docs/jellyfish/api/spv.md +++ b/website/docs/jellyfish/api/spv.md @@ -108,3 +108,23 @@ interface DecodeHtlcResult { hash: string } ``` + +## claimHtlc + +Claims all coins in HTLC address. + +```ts title="client.spv.claimHtlc()" +interface spv { + claimHtlc (scriptAddress: string, destinationAddress: string, options: ClaimHtlcOptions): Promise +} + +interface ClaimHtlcOptions { + seed: string + feeRate?: BigNumber +} + +interface SendMessageResult { + txid: string + sendmessage: string +} +```