Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/DeFiCh/jellyfish into chore…
Browse files Browse the repository at this point in the history
…/test_1.7.x
  • Loading branch information
monstrobishi committed May 25, 2021
2 parents d18480c + 159c913 commit e47b258
Show file tree
Hide file tree
Showing 25 changed files with 1,056 additions and 12 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ jobs:
with:
node-version: '15'

- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run build

Expand All @@ -31,6 +38,13 @@ jobs:
with:
node-version: '15'

- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run build
- run: npx --no-install ts-standard
Expand All @@ -44,6 +58,13 @@ jobs:
with:
node-version: '15'

- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- uses: andresz1/size-limit-action@c53e18c847d5eb13f61754c45f3fbfc3aa5c17cc
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
with:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"publish:latest": "lerna exec -- npm publish --tag latest --access public",
"standard": "ts-standard --fix",
"test": "jest",
"test:ci": "jest --ci --coverage --forceExit",
"test:ci": "jest --ci --coverage --forceExit --maxWorkers=4",
"all": "npm run build && npm run standard && npm run test"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ describe('masternode', () => {
})
})

describe('getdifficulty', () => {
it('should getDifficulty', async () => {
const difficulty = await client.blockchain.getDifficulty()
expect(difficulty).toBeGreaterThanOrEqual(0)
})
})

describe('getRawMempool', () => {
beforeAll(async () => {
await client.wallet.setWalletFlag(wallet.WalletFlag.AVOID_REUSE)
Expand Down
140 changes: 139 additions & 1 deletion packages/jellyfish-api-core/__tests__/category/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContainerAdapterClient } from '../container_adapter_client'
import { MasterNodeRegTestContainer, RegTestContainer } from '@defichain/testcontainers'
import { BigNumber, wallet } from '../../src'
import waitForExpect from 'wait-for-expect'
import { UTXO, ListUnspentOptions, WalletFlag, SendToAddressOptions, Mode } from '../../src/category/wallet'
import { UTXO, ListUnspentOptions, WalletFlag, SendToAddressOptions, Mode, SendManyOptions } from '../../src/category/wallet'

describe('non masternode', () => {
const container = new RegTestContainer()
Expand Down Expand Up @@ -632,3 +632,141 @@ describe('masternode', () => {
})
})
})

describe('sendMany', () => {
// NOTE(surangap): defid side(c++) does not have much tests for sendmany RPC atm.
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await container.waitForReady()
await container.waitForWalletCoinbaseMaturity()
await container.waitForWalletBalanceGTE(101)
})

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

// Returns matching utxos for given transaction id and address.
const getMatchingUTXO = async (txId: string, address: string): Promise<UTXO[]> => {
const options: ListUnspentOptions = {
addresses: [address]
}

const utxos: UTXO[] = await client.wallet.listUnspent(1, 9999999, options)
return utxos.filter((utxo) => {
return (utxo.address === address) && (utxo.txid === txId)
})
}

it('should send one address using sendMany', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001 }
const transactionId = await client.wallet.sendMany(amounts)
expect(typeof transactionId).toBe('string')

// generate one block
await container.generate(1)

// check the corresponding UTXO
const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
// In this case the we should only have one matching utxo
expect(utxos.length).toStrictEqual(1)
utxos.forEach(utxo => {
expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
expect(utxo.amount).toStrictEqual(new BigNumber(0.00001))
})
})

it('should send multiple address using sendMany', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 }
const transactionId = await client.wallet.sendMany(amounts)
expect(typeof transactionId).toBe('string')

// generate one block
await container.generate(1)

// check the corresponding UTXOs
const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
// In this case the we should only have one matching utxo
expect(utxos.length).toStrictEqual(1)
utxos.forEach(utxo => {
expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
expect(utxo.amount).toStrictEqual(new BigNumber(0.00001))
})

const utxos2 = await getMatchingUTXO(transactionId, 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy')
// In this case the we should only have one matching utxo
expect(utxos2.length).toStrictEqual(1)
utxos2.forEach(utxo => {
expect(utxo.address).toStrictEqual('mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy')
expect(utxo.amount).toStrictEqual(new BigNumber(0.00002))
})
})

it('should sendMany with comment', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 }
const options: SendManyOptions = {
comment: 'test comment'
}
const transactionId = await client.wallet.sendMany(amounts, [], options)
expect(typeof transactionId).toBe('string')
})

it('should sendMany with replaceable', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 }
const options: SendManyOptions = {
replaceable: true
}
const transactionId = await client.wallet.sendMany(amounts, [], options)
expect(typeof transactionId).toBe('string')
})

it('should sendMany with confTarget', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 }
const options: SendManyOptions = {
confTarget: 60
}
const transactionId = await client.wallet.sendMany(amounts, [], options)
expect(typeof transactionId).toBe('string')
})

it('should sendMany with estimateMode', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 }
const options: SendManyOptions = {
estimateMode: Mode.ECONOMICAL
}
const transactionId = await client.wallet.sendMany(amounts, [], options)
expect(typeof transactionId).toBe('string')
})

it('should sendMany with fee substracted from mentioned recipients', async () => {
const amounts: Record<string, number> = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 10.5 }
const subtractFeeFrom: string [] = ['mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy']
const transactionId = await client.wallet.sendMany(amounts, subtractFeeFrom)
expect(typeof transactionId).toBe('string')

// generate one block
await container.generate(1)

// check the corresponding UTXOs
const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
// In this case the we should only have one matching utxo
expect(utxos.length).toStrictEqual(1)
utxos.forEach(utxo => {
expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU')
// amount should be equal to 0.00001
expect(utxo.amount).toStrictEqual(new BigNumber(0.00001))
})

const utxos2 = await getMatchingUTXO(transactionId, 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy')
// In this case the we should only have one matching utxo
expect(utxos2.length).toStrictEqual(1)
utxos2.forEach(utxo => {
expect(utxo.address).toStrictEqual('mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy')
// amount should be less than 10.5
expect(utxo.amount.isLessThan(10.5)).toBe(true)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class ContainerAdapterClient extends ApiClient {
})

if (error != null) {
throw new RpcApiError({ ...error, rpcMethod: method })
throw new RpcApiError({ ...error, method: method })
}

return result
Expand Down
9 changes: 9 additions & 0 deletions packages/jellyfish-api-core/src/category/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ export class Blockchain {
return await this.client.call('getchaintips', [], 'number')
}

/**
* Get the proof-of-work difficulty as a multiple of the minimum difficulty.
*
* @return {Promise<number>}
*/
async getDifficulty (): Promise<number> {
return await this.client.call('getdifficulty', [], 'number')
}

/**
* Get details of unspent transaction output (UTXO).
*
Expand Down
43 changes: 43 additions & 0 deletions packages/jellyfish-api-core/src/category/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,42 @@ export class Wallet {
async listAddressGroupings (): Promise<any[][][]> {
return await this.client.call('listaddressgroupings', [], 'bignumber')
}

/**
* Send given amounts to multiple given address and return a transaction id.
*
* @param {Record<string, number>} amounts Dictionary/map with individual addresses and amounts
* @param {string[]} subtractfeefrom Array of addresses from which fee needs to be deducted.
* @param {SendManyOptions} options
* @param {string} [options.comment] A comment
* @param {boolean} [options.replaceable] Allow this transaction to be replaced by a transaction with higher fees via BIP 125
* @param {number} [options.confTarget] Confirmation target (in blocks)
* @param {Mode} [options.estimateMode] The fee estimate mode, must be one of (Mode.UNSET, Mode.ECONOMICAL, Mode.CONSERVATIVE)
* @return {Promise<string>} hex string of the transaction
*/
async sendMany (
amounts: Record<string, number>,
subtractfeefrom: string [] = [],
options: SendManyOptions = {}): Promise<string> {
const {
comment = '',
replaceable = false,
confTarget = 6,
estimateMode = Mode.UNSET
} = options

const dummy: string = '' // Must be set to '' for backward compatibality.
const minconf: number = 0 // Ignored dummy value

return await this.client.call(
'sendmany',
[
dummy, amounts, minconf, comment, subtractfeefrom,
replaceable, confTarget, estimateMode
],
'bignumber'
)
}
}

export interface UTXO {
Expand Down Expand Up @@ -263,6 +299,13 @@ export interface SendToAddressOptions {
avoidReuse?: boolean
}

export interface SendManyOptions {
comment?: string
replaceable?: boolean
confTarget?: number
estimateMode?: Mode
}

export interface CreateWalletResult {
name: string
warning: string
Expand Down
6 changes: 3 additions & 3 deletions packages/jellyfish-api-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ export class ClientApiError extends ApiError {
* API RPC error, from upstream.
*/
export class RpcApiError extends ApiError {
public readonly payload: { code: number, message: string, rpcMethod: string }
public readonly payload: { code: number, message: string, method: string }

constructor (error: { code: number, message: string, rpcMethod: string }) {
super(`RpcApiError: '${error.message}', code: ${error.code}, rpcMethod: ${error.rpcMethod}`)
constructor (error: { code: number, message: string, method: string }) {
super(`RpcApiError: '${error.message}', code: ${error.code}, method: ${error.method}`)
this.payload = error
}
}
2 changes: 1 addition & 1 deletion packages/jellyfish-api-jsonrpc/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('ApiError', () => {

await expect(promise).rejects.toThrow(ApiError)
await expect(promise).rejects.toThrow(RpcApiError)
await expect(promise).rejects.toThrow('RpcApiError: \'Invalid private key encoding\', code: -5')
await expect(promise).rejects.toThrow(/^RpcApiError: 'Invalid private key encoding', code: -5, method: importprivkey$/)
})

it('should throw ClientApiError: 404 - Not Found', async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/jellyfish-api-jsonrpc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class JsonRpcClient extends ApiClient {
switch (response.status) {
case 200:
default:
return JsonRpcClient.parse(text, precision)
return JsonRpcClient.parse(method, text, precision)

case 401:
case 404:
Expand All @@ -84,13 +84,13 @@ export class JsonRpcClient extends ApiClient {
})
}

private static parse (text: string, precision: Precision | PrecisionPath): any {
private static parse (method: string, text: string, precision: Precision | PrecisionPath): any {
const { result, error } = JellyfishJSON.parse(text, {
result: precision
})

if (error != null) {
throw new RpcApiError({ ...error, rpcMethod: text })
throw new RpcApiError({ ...error, method: method })
}

return result
Expand Down
Loading

0 comments on commit e47b258

Please sign in to comment.