Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jellyfish-api-core): implement refundHtlcAll #1324

Merged
merged 4 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
surangap marked this conversation as resolved.
Show resolved Hide resolved
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 not htlc is created for wallet', async () => {
cwkang1998 marked this conversation as resolved.
Show resolved Hide resolved
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