Skip to content

Commit

Permalink
feat(jellyfish-api-core): implement refundHtlcAll (#1324)
Browse files Browse the repository at this point in the history
* feat(jellyfish-api-core): implement refundHtlcAll

* feat(jellyfish-api-core): change refundHtlcAll to return string arr
  • Loading branch information
cwkang1998 authored Apr 22, 2022
1 parent 4cb0d98 commit 491b017
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 0 deletions.
14 changes: 14 additions & 0 deletions docs/node/CATEGORIES/14-spv.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ interface SendMessageResult {
}
```

## refundHtlcAll

Gets all HTLC contracts stored in wallet and creates refunds transactions for all that have expired

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

interface SpvDefaultOptions {
feeRate?: BigNumber
}
```

## listHtlcOutputs

List all outputs related to HTLC addresses in the wallet.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { BigNumber, RpcApiError } from '@defichain/jellyfish-api-core'
import { Testing } from '@defichain/jellyfish-testing'

describe('Spv', () => {
let testing: Testing
let container: MasterNodeRegTestContainer

beforeEach(async () => {
testing = Testing.create(new MasterNodeRegTestContainer())
container = testing.container
await container.start()
await container.spv.fundAddress(await container.call('spv_getnewaddress')) // Funds 1 BTC
})

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

it('should refundHtlcAll', 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 htlc1 = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])
const htlc2 = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

const fund1 = await container.call('spv_sendtoaddress', [htlc1.address, 0.1]) // Funds HTLC address
const fund2 = await container.call('spv_sendtoaddress', [htlc2.address, 0.2])
await container.spv.increaseSpvHeight(10)

const destinationAddress = await container.call('spv_getnewaddress')
const results = await testing.rpc.spv.refundHtlcAll(destinationAddress) // This refund should only happen after timeout threshold set in createHtlc. See https://en.bitcoin.it/wiki/BIP_0199
expect(results.length).toStrictEqual(1)
for (const txid of results) {
expect(typeof txid).toStrictEqual('string')
expect(txid.length).toStrictEqual(64)
}

/**
* Assert that refund happened
* confirms at 11 as it `increaseSpvHeight` increased height by 10,
* and refund action adds 1.
*/
const outputList: any[] = await container.call('spv_listhtlcoutputs')
expect(outputList.length).toStrictEqual(2)
const output1 = outputList.find(({ txid }: { txid: string }) => txid === fund1.txid)
const output2 = outputList.find(({ txid }: { txid: string }) => txid === fund2.txid)
expect(output1).toStrictEqual({
txid: fund1.txid,
vout: expect.any(Number),
amount: new BigNumber(0.1).toNumber(),
address: htlc1.address,
confirms: 11,
spent: {
txid: results[0],
confirms: 1
}
})
expect(output2).toStrictEqual({
txid: fund2.txid,
vout: expect.any(Number),
amount: new BigNumber(0.2).toNumber(),
address: htlc2.address,
confirms: 11,
spent: {
txid: results[0],
confirms: 1
}
})

/**
* Assert that the destination address received the refund
*/
const listReceivingAddresses: any[] = 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 === results[0])).toStrictEqual(true)
})

it('should refundHtlcAll for expired only', 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 htlc1 = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])
const htlc2 = await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '50'])

const fund1 = await container.call('spv_sendtoaddress', [htlc1.address, 0.1]) // Funds HTLC address
const fund2 = await container.call('spv_sendtoaddress', [htlc2.address, 0.2])
await container.spv.increaseSpvHeight(10)

const destinationAddress = await container.call('spv_getnewaddress')
const results = await testing.rpc.spv.refundHtlcAll(destinationAddress) // This refund should only happen after timeout threshold set in createHtlc. See https://en.bitcoin.it/wiki/BIP_0199
expect(results.length).toStrictEqual(1)
for (const txid of results) {
expect(typeof txid).toStrictEqual('string')
expect(txid.length).toStrictEqual(64)
}

/**
* Assert that refund happened
* confirms at 11 as it `increaseSpvHeight` increased height by 10,
* and refund action adds 1.
*/
const outputList: any[] = await container.call('spv_listhtlcoutputs')
expect(outputList.length).toStrictEqual(2)
const output1 = outputList.find(({ txid }: { txid: string }) => txid === fund1.txid)
const output2 = outputList.find(({ txid }: { txid: string }) => txid === fund2.txid)
expect(output1).toStrictEqual({
txid: fund1.txid,
vout: expect.any(Number),
amount: new BigNumber(0.1).toNumber(),
address: htlc1.address,
confirms: 11,
spent: {
txid: results[0],
confirms: 1
}
})
expect(output2).toStrictEqual({
txid: fund2.txid,
vout: expect.any(Number),
amount: new BigNumber(0.2).toNumber(),
address: htlc2.address,
confirms: 11
})

/**
* Assert that the destination address received the refund
*/
const listReceivingAddresses: any[] = 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 === results[0])).toStrictEqual(true)
})

it('should refundHtlcAll 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'])

const fund = await container.call('spv_sendtoaddress', [htlc.address, 0.1]) // Funds HTLC address
await container.spv.increaseSpvHeight()

const destinationAddress = await container.call('spv_getnewaddress')
const results = await testing.rpc.spv.refundHtlcAll(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(results.length).toStrictEqual(1)
for (const txid of results) {
expect(typeof txid).toStrictEqual('string')
expect(txid.length).toStrictEqual(64)
}

/**
* Assert that refund happened
* confirms at 11 as it `increaseSpvHeight` increased height by 10,
* and refund action adds 1.
*/
const outputList: any[] = await container.call('spv_listhtlcoutputs')
expect(outputList.length).toStrictEqual(1)
const output = outputList.find(({ txid }: { txid: string }) => txid === fund.txid)
expect(output).toStrictEqual({
txid: fund.txid,
vout: expect.any(Number),
amount: new BigNumber(0.1).toNumber(),
address: htlc.address,
confirms: 11,
spent: {
txid: results[0],
confirms: 1
}
})

/**
* 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 === results[0])).toStrictEqual(true)
})

it('should not refundHtlcAll 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')])
await container.call('spv_createhtlc', [pubKeyA, pubKeyB, '10'])

const promise = testing.rpc.spv.refundHtlcAll(await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow('No unspent HTLC outputs found')
})

it('should not refundHtlcAll 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'])

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

const promise = testing.rpc.spv.refundHtlcAll('XXXX')
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow('Invalid destination address')
})

it('should not refundHtlcAll 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 testing.container.call('spv_sendtoaddress', [htlc.address, 0.00000546]) // Funds HTLC address with dust

const promise = testing.rpc.spv.refundHtlcAll(await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow('No unspent HTLC outputs found')
})

it('should not refundHtlcAll when no htlc is created for wallet', async () => {
const promise = testing.rpc.spv.refundHtlcAll(await container.call('spv_getnewaddress'))
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow('Redeem script details not found.')
})
})
17 changes: 17 additions & 0 deletions packages/jellyfish-api-core/src/category/spv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ export class Spv {
return await this.client.call('spv_refundhtlc', [scriptAddress, destinationAddress, options.feeRate], 'number')
}

/**
* Gets all HTLC contracts stored in wallet and creates refunds transactions for all that have expired
*
* @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<string[]>} array of txid
*/
async refundHtlcAll (destinationAddress: string, options: SpvDefaultOptions = { feeRate: new BigNumber('10000') }): Promise<string[]> {
/**
* Looking at ain, its returning an array of txid containing only 1 txid.
* Considering some factors, this implementation is different from the rpc docs
* on ain side. Refer to PR https://github.com/DeFiCh/jellyfish/pull/1324
*/
return await this.client.call<string[]>('spv_refundhtlcall', [destinationAddress, options.feeRate], 'number')
}

/**
* List all outputs related to HTLC addresses in the wallet.
*
Expand Down

0 comments on commit 491b017

Please sign in to comment.