From 38a1a1991aca6233f6fdc4290bd160f7011e8324 Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Mon, 12 Apr 2021 20:26:52 +0800 Subject: [PATCH 1/5] completed 4 rpc in rawtx.ts --- .idea/dictionaries/fuxing.xml | 28 ++- .../__tests__/category/rawtx.test.ts | 67 ++++++ .../jellyfish-api-core/src/category/rawtx.ts | 209 ++++++++++++++++++ packages/jellyfish-api-core/src/index.ts | 3 + 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/rawtx.test.ts create mode 100644 packages/jellyfish-api-core/src/category/rawtx.ts diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml index b308f9ed87..f71599a5ec 100644 --- a/.idea/dictionaries/fuxing.xml +++ b/.idea/dictionaries/fuxing.xml @@ -10,10 +10,13 @@ bayfrontmarinaheight bcrt bech + bestblock + bestblockhash booland boolor canonbrother chainparams + chainwork checklocktimeverify checkmultisig checkmultisigverify @@ -23,6 +26,7 @@ clarkequayheight codeseparator createmasternode + createrawtransaction currentblocktx currentblockweight dakotacrescentheight @@ -34,6 +38,7 @@ devnet dockerode dummypos + dumpprivkey equalverify fromaltstack fullstackninja @@ -41,6 +46,8 @@ generatetoaddress getaddressinfo getbalance + getblock + getblockchaininfo getblockcount getblockhash getmintinginfo @@ -48,12 +55,14 @@ getnewaddress getreceivedbyaddress gettransaction + gettxout getunconfirmedbalance greaterthan greaterthanorequal ifdup importprivkey infima + initialblockdownload invalidopcode isoperator jsonrpc @@ -61,6 +70,7 @@ lessthanorequal libevent listaccounts + locktime logtimemicros lshift mainnet @@ -68,9 +78,13 @@ masternodeid masternodeoperator masternodestate + mediantime + mempool + merkleroot mintedblocks nblocks networkhashps + nextblockhash nmasternode nospv notif @@ -78,12 +92,14 @@ numequalverify numnotequal pooledtx + previousblockhash prevout prevouts printtoconsole priv pubkey pushdata + rawtx regtest rewardaddress ripemd @@ -94,26 +110,36 @@ rshift secp segwit + sendrawtransaction sendtoaddress sighash sighashtype signmessage + signrawtransactionwithkey + sigs + softforks + strippedsize testcontainers + testmempoolaccept thedoublejay toaltstack txid txindex + txinwitness txnotokens uacomment unpkg utxo + utxos utxostoaccount varint verif + verificationprogress verifymessage vernotif vout + vsize wpkh - \ No newline at end of file + diff --git a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts new file mode 100644 index 0000000000..f8869ac7e3 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts @@ -0,0 +1,67 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { ContainerAdapterClient } from '../container_adapter_client' +import waitForExpect from 'wait-for-expect' +import { BigNumber } from '../../src' + +const container = new MasterNodeRegTestContainer() +const client = new ContainerAdapterClient(container) + +beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + + // Wait for > balance for testing transaction + await waitForExpect(async () => { + const balance: BigNumber = await client.wallet.getBalance() + expect(balance.isGreaterThan(new BigNumber('300'))).toBe(true) + }) +}) + +afterAll(async () => { + await container.stop() +}) + +describe('createRawTransaction', () => { + const bech32 = 'bcrt1qvy72dqwy66xa5kkas86zuruanzqslq80le4drc' + const privKey = 'cR5AWXgDrZXkXbB74cHXY5dpGwg6pPAHwSJgXtgFRodasjduoNiJ' + const funded = new BigNumber('10') + + beforeEach(async () => { + const txid = await container.call('sendtoaddress', [bech32, funded]) + + // TODO(fuxingloh): send to address + // TODO(fuxingloh): setup unspent into a new wallet for testing + }) + + it('should createRawTransaction()', () => { + + }); +}) + +describe('signRawTransactionWithKey', () => { + const bech32 = 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7' + const privKey = 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' +}) + +describe('testMempoolAccept', () => { + const bech32 = 'bcrt1q0860seu9wkczcrrc80apms6eecfyy0lycfgqcm' + const privKey = 'cPYesPZwbm89k3cJrRRVU3apCnLPxGHzzeLAr9M2BNHQE4pcEjNm' +}) + +describe('sendRawTransaction', () => { + const bech32 = 'bcrt1qnadpmd2596kw4r7n02pz927jfnaq7y3qcvkcv2' + const privKey = 'cUhK4NkExCaBV4MxhR7vqLkRk5ekUFdn2h9Y3qUNBSCBQu11q1SH' +}) + +describe('lifecycle stateful unspent, queried', () => { + const bech32 = 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn' + const privKey = 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' +}) + +describe('lifecycle stateless unspent, self tracked', () => { + const bech32 = 'bcrt1qg6qmejx224dqwl7zl5gwr8mn57fwx5lppmqen6' + const privKey = 'cVEEgcoxP7cPB23ZM1Y2k33z66rABjbtFZ6AVRKai1XiamZ3Fe5z' +}) + + diff --git a/packages/jellyfish-api-core/src/category/rawtx.ts b/packages/jellyfish-api-core/src/category/rawtx.ts new file mode 100644 index 0000000000..691742b751 --- /dev/null +++ b/packages/jellyfish-api-core/src/category/rawtx.ts @@ -0,0 +1,209 @@ +import { BigNumber, ApiClient } from '../.' + +export enum SigHashType { + ALL = 'ALL', + NONE = 'NONE', + SINGLE = 'SINGLE', + ALL_ANYONECANPAY = 'ALL|ANYONECANPAY', + NONE_ANYONECANPAY = 'NONE|ANYONECANPAY', + SINGLE_ANYONECANPAY = 'SINGLE|ANYONECANPAY', +} + +/** + * RawTransaction RPCs for DeFi Blockchain + */ +export class RawTx { + private readonly client: ApiClient + + constructor (client: ApiClient) { + this.client = client + } + + private static asHex (rawTx: string | Buffer): string { + return Buffer.isBuffer(rawTx) ? rawTx.toString('hex') : rawTx + } + + /** + * Create a transaction spending the given inputs and creating new outputs. + * Returns hex-encoded raw transaction. + * Note that the transaction's inputs are not signed, and + * it is not stored in the wallet or transmitted to the network. + * + * @param inputs {CreateRawTxIn[]} array of inputs + * @param outputs {CreateRawTxOut[]} array with outputs + * @param options {CreateRawTxOptions} + * @param options.locktime {number} Non-0 value also locktime-activates inputs + * @param options.replaceable {boolean} Marks this transaction as BIP125-replaceable + * @return {Promise} hex string of the transaction (Little Endian) + */ + async createRawTransaction ( + inputs: CreateRawTxIn[], + outputs: CreateRawTxOut[], + options: CreateRawTxOptions = {} + ): Promise { + const { locktime = 0, replaceable = false } = options + + return await this.client.call('createrawtransaction', [ + inputs, outputs, locktime, replaceable + ], 'number') + } + + /** + * Sign inputs for raw transaction (serialized, hex-encoded). + * The second argument is an array of base58-encoded private + * keys that will be the only keys used to sign the transaction. + * The third optional argument (may be null) is an array of previous transaction outputs that + * this transaction depends on but may not yet be in the block chain. + * + * @param rawTx {string|Buffer} unsigned raw transaction + * @param privKeys {string[] | Buffer[]} array of base58-encoded private keys for signing (WIF) + * @param prevTxs {SignRawTxWithKeyPrevTx[]} array of previous dependent transaction outputs + * @param options {SignRawTxWithKeyOptions} + * @param options.sigHashType {SigHashType} The signature hash type to use + * @return {Promise} + */ + async signRawTransactionWithKey ( + rawTx: string | Buffer, + privKeys: string[], + prevTxs: SignRawTxWithKeyPrevTx[], + options: SignRawTxWithKeyOptions = {} + ): Promise { + const hex = RawTx.asHex(rawTx) + const { sigHashType = SigHashType.ALL } = options + return await this.client.call('signrawtransactionwithkey', [ + hex, privKeys, prevTxs, sigHashType + ], 'number') + } + + /** + * Returns result of mempool acceptance tests indicating if raw transaction would be accepted by mempool. + * This checks if the transaction violates the consensus or policy rules. + * + * @param signedTx {string|Buffer} signed raw transaction + * @param maxFeeRate {BigNumber} Reject transactions whose fee rate is higher than the specified value. + * @return {Promise} transaction mempool accept result + * @see sendRawTransaction + * @see createRawTransaction + * @see signRawTransactionWithKey + */ + async testMempoolAccept (signedTx: string | Buffer, maxFeeRate: BigNumber = new BigNumber('0')): Promise { + const hex = RawTx.asHex(signedTx) + const results: TestMempoolAcceptResult[] = await this.client.call('testmempoolaccept', [ + [hex], maxFeeRate + ], 'number') + return results[0] + } + + /** + * Submit a raw transaction (serialized, hex-encoded) to connected node and network. + * Note that the transaction will be sent unconditionally to all peers, so using this + * for manual rebroadcast may degrade privacy by leaking the transaction's origin, as + * nodes will normally not rebroadcast non-wallet transactions already in their mempool. + * + * @param signedTx {string|Buffer} signed raw transaction + * @param maxFeeRate {BigNumber} Reject transactions whose fee rate is higher than the specified value. + * @return {Promise} transaction hash in hex + * @see testMempoolAccept + * @see createRawTransaction + * @see signRawTransactionWithKey + */ + async sendRawTransaction (signedTx: string | Buffer, maxFeeRate: BigNumber = new BigNumber('0')): Promise { + const hex = RawTx.asHex(signedTx) + return await this.client.call('sendrawtransaction', [ + hex, maxFeeRate + ], 'number') + } +} + +export interface CreateRawTxOptions { + locktime?: number + replaceable?: boolean +} + +export interface CreateRawTxIn { + txid: string + vout: number + sequence?: number +} + +export interface CreateRawTxOut { + [address: string]: BigNumber +} + +export interface SignRawTxWithKeyPrevTx { + /** + * The transaction id + */ + txid: string + /** + * The output number + */ + vout: number + /** + * Pubkey + */ + scriptPubKey: string + /** + * required for P2SH or P2WSH + */ + redeemScript?: string + /** + * The amount spent + */ + amount: BigNumber +} + +export interface SignRawTxWithKeyOptions { + sigHashType?: SigHashType +} + +export interface SignRawTxWithKeyResult { + /** + * The hex-encoded raw transaction with signature(s) + */ + hex: string + /** + * If the transaction has a complete set of signatures + */ + complete: boolean + /** + * Script verification errors (if there are any) + */ + errors: Array<{ + /** + * The hash of the referenced, previous transaction + */ + txid: string + /** + * The index of the output to spent and used as input + */ + vout: number + /** + * The hex-encoded signature script + */ + scriptSig: string + /** + * Script sequence number + */ + sequence: number + /** + * Verification or signing error related to the input + */ + error: string + }> +} + +export interface TestMempoolAcceptResult { + /** + * The transaction hash in hex + */ + txid: string + /** + * If the mempool allows this tx to be inserted + */ + allowed: boolean + /** + * Rejection string, only present when 'allowed' is false + */ + ['reject-reason']?: string +} diff --git a/packages/jellyfish-api-core/src/index.ts b/packages/jellyfish-api-core/src/index.ts index b5086e61cc..491f363415 100644 --- a/packages/jellyfish-api-core/src/index.ts +++ b/packages/jellyfish-api-core/src/index.ts @@ -1,11 +1,13 @@ import { Precision, PrecisionPath } from '@defichain/jellyfish-json' import { Blockchain } from './category/blockchain' import { Mining } from './category/mining' +import { RawTx } from './category/rawtx' import { Wallet } from './category/wallet' export * from '@defichain/jellyfish-json' export * from './category/blockchain' export * from './category/mining' +export * as rawtx from './category/rawtx' export * from './category/wallet' /** @@ -14,6 +16,7 @@ export * from './category/wallet' export abstract class ApiClient { public readonly blockchain = new Blockchain(this) public readonly mining = new Mining(this) + public readonly rawtx = new RawTx(this) public readonly wallet = new Wallet(this) /** From cce584b566f0151673c94045d79ec8ddf0283a65 Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Thu, 15 Apr 2021 17:27:05 +0800 Subject: [PATCH 2/5] sendRawTransaction testMempoolAccept createRawTransaction signRawTransactionWithKey --- .../__tests__/category/rawtx.test.ts | 275 ++++++++++++++++-- .../jellyfish-api-core/src/category/rawtx.ts | 65 ++--- 2 files changed, 274 insertions(+), 66 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts index f8869ac7e3..e9148cb883 100644 --- a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts @@ -1,7 +1,7 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../container_adapter_client' -import waitForExpect from 'wait-for-expect' import { BigNumber } from '../../src' +import { CreateRawTxOut, SigHashType, SignRawTxWithKeyResult, TestMempoolAcceptResult } from '../../src/category/rawtx' const container = new MasterNodeRegTestContainer() const client = new ContainerAdapterClient(container) @@ -10,58 +10,269 @@ beforeAll(async () => { await container.start() await container.waitForReady() await container.waitForWalletCoinbaseMaturity() - - // Wait for > balance for testing transaction - await waitForExpect(async () => { - const balance: BigNumber = await client.wallet.getBalance() - expect(balance.isGreaterThan(new BigNumber('300'))).toBe(true) - }) + await container.waitForWalletBalanceGTE(300) }) afterAll(async () => { await container.stop() }) +// From Address P2WPKH +const input = { + bech32: 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn', + privKey: 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' +} +// To Address P2WPKH +const output = { + bech32: 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7', + privKey: 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' +} + describe('createRawTransaction', () => { - const bech32 = 'bcrt1qvy72dqwy66xa5kkas86zuruanzqslq80le4drc' - const privKey = 'cR5AWXgDrZXkXbB74cHXY5dpGwg6pPAHwSJgXtgFRodasjduoNiJ' - const funded = new BigNumber('10') + it('should createRawTransaction()', async () => { + const { txid } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: 0 }] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + + const unsigned: string = await client.rawtx.createRawTransaction(inputs, outputs) + + // Version + expect(unsigned.substr(0, 8)).toBe('04000000') + // Vin + expect(unsigned.substr(8, 2)).toBe('01') + expect(unsigned.substr(10, 64)).toBe( + Buffer.from(txid, 'hex').reverse().toString('hex') + ) + expect(unsigned.substr(74, 8)).toBe('00000000') + expect(unsigned.substr(82, 2)).toBe('00') + expect(unsigned.substr(84, 8)).toBe('ffffffff') + // Vout + expect(unsigned.substr(92, 2)).toBe('01') + expect(unsigned.substr(94, 16)).toBe('0065cd1d00000000') + expect(unsigned.substr(110, 2)).toBe('16') + expect(unsigned.substr(112, 2)).toBe('00') // OP_0 + expect(unsigned.substr(114, 2)).toBe('14') + expect(unsigned.substr(116, 40)).toBe('4ab4391ce5a732e36139e72d79a28e01b7b08034') // PKH + expect(unsigned.substr(156, 2)).toBe('00') // DCT_ID + // LockTime + expect(unsigned.substr(158, 8)).toBe('00000000') + + expect(unsigned.length).toBe(166) + }) - beforeEach(async () => { - const txid = await container.call('sendtoaddress', [bech32, funded]) + it('should createRawTransaction() with locktime 1000', async () => { + const { txid } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: 0 }] - // TODO(fuxingloh): send to address - // TODO(fuxingloh): setup unspent into a new wallet for testing + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + + const unsigned: string = await client.rawtx.createRawTransaction(inputs, outputs, { + locktime: 1000 + }) + + expect(unsigned.substr(0, 84)).toBe( + '0400000001' + Buffer.from(txid, 'hex').reverse().toString('hex') + '0000000000' + ) + expect(unsigned.substr(84, 8)).toBe('feffffff') + expect(unsigned.substr(92, 66)).toBe('010065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b0803400') + expect(unsigned.substr(158, 8)).toBe('e8030000') + expect(unsigned.length).toBe(166) }) - it('should createRawTransaction()', () => { + it('should createRawTransaction() with replaceable = true', async () => { + const { txid } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: 0 }] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + + const unsigned: string = await client.rawtx.createRawTransaction(inputs, outputs, { + replaceable: true + }) - }); + expect(unsigned.substr(0, 84)).toBe( + '0400000001' + Buffer.from(txid, 'hex').reverse().toString('hex') + '0000000000' + ) + expect(unsigned.substr(84, 8)).toBe('fdffffff') + expect(unsigned.substr(92, 74)).toBe('010065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340000000000') + expect(unsigned.length).toBe(166) + }) }) describe('signRawTransactionWithKey', () => { - const bech32 = 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7' - const privKey = 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' + it('should signRawTransactionWithKey() 10.0 to 5.0 with 5.0 as fee', async () => { + const { txid, vout } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: vout }] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + + const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) + const signed = await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey]) + + expect(signed.complete).toBe(true) + expect(signed.hex.substr(0, 14)).toBe('04000000000101') + expect(signed.hex.substr(86, 88)).toBe('00ffffffff010065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b0803400024730440220') + expect(signed.hex.substr(306, 78)).toBe('012103987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe8100000000') + expect(signed.hex.length).toBe(384) + }) + + // TODO(anyone): SignRawTxWithKeyPrevTx is not yet typed tested, + // for sake of time. It's out of my scope of work. + + it('should signRawTransactionWithKey() 10.0 to 5.0 with 4.9 as change and 0.1 as fee', async () => { + const { txid, vout } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: vout }] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + outputs[input.bech32] = new BigNumber('4.9') + + const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) + const signed = await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey]) + + expect(signed.complete).toBe(true) + expect(signed.hex.substr(0, 14)).toBe('04000000000101') + expect(signed.hex.substr(86, 152)).toBe('00ffffffff020065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340080ce341d0000000016001425a544c073cbca4e88d59f95ccd52e584c7e6a8200024730440220') + expect(signed.hex.substr(370, 78)).toBe('012103987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe8100000000') + expect(signed.hex.length).toBe(448) + }) + + describe('signRawTransactionWithKeySigHashType', () => { + async function signRawTransactionWithKeySigHashType (type: SigHashType): Promise { + const inputs = [ + await container.fundAddress(input.bech32, 10) + ] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('5') + outputs[input.bech32] = new BigNumber('4.9') + + const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) + return await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey], [], { + sigHashType: type + }) + } + + it('should signRawTransactionWithKey() with SigHashType.ALL', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.ALL) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + + it('should signRawTransactionWithKey() with SigHashType.NONE', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.NONE) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + + it('should signRawTransactionWithKey() with SigHashType.SINGLE', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.SINGLE) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + + it('should signRawTransactionWithKey() with SigHashType.ALL_ANYONECANPAY', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.ALL_ANYONECANPAY) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + + it('should signRawTransactionWithKey() with SigHashType.NONE_ANYONECANPAY', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.NONE_ANYONECANPAY) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + + it('should signRawTransactionWithKey() with SigHashType.SINGLE_ANYONECANPAY', async () => { + const signed = await signRawTransactionWithKeySigHashType(SigHashType.SINGLE_ANYONECANPAY) + + expect(signed.complete).toBe(true) + expect(signed.hex.length).toBe(448) + }) + }) }) describe('testMempoolAccept', () => { - const bech32 = 'bcrt1q0860seu9wkczcrrc80apms6eecfyy0lycfgqcm' - const privKey = 'cPYesPZwbm89k3cJrRRVU3apCnLPxGHzzeLAr9M2BNHQE4pcEjNm' -}) + it('testMempoolAccept() should fail with random hex', async () => { + const invalid = 'bf94838ced5a8313eb355c3bdd053cdbdbb3f9e0' + return await expect(client.rawtx.testMempoolAccept(invalid)) + .rejects.toThrow('RpcApiError: \'TX decode failed\', code: -22') + }) -describe('sendRawTransaction', () => { - const bech32 = 'bcrt1qnadpmd2596kw4r7n02pz927jfnaq7y3qcvkcv2' - const privKey = 'cUhK4NkExCaBV4MxhR7vqLkRk5ekUFdn2h9Y3qUNBSCBQu11q1SH' -}) + it('testMempoolAccept() should fail due to missing-inputs', async () => { + const signed = '0400000000010193c90783761bf94838ced5a8313eb355c3bdd053cdbdbb3f9e0f3dbc3243609b0000000000ffffffff020065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340080ce341d0000000016001425a544c073cbca4e88d59f95ccd52e584c7e6a82000247304402201142c461b7b52323654710b14074928dd8e623d75141f9eb8c2132b7cb2d47c202202883fde993e1ecf0cf3955235522e9fe948b523b568d0e6b427f83c6f1b3efd9012103987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe8100000000' + const result = await client.rawtx.testMempoolAccept(signed) -describe('lifecycle stateful unspent, queried', () => { - const bech32 = 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn' - const privKey = 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' -}) + expect(result.txid).toBe('5749ad89256b50786a02d4527621a4fc7fa6acc5a3b289841112628ff3a4990a') + expect(result.allowed).toBe(false) + expect(result['reject-reason']).toBe('missing-inputs') + }) + + async function testMempoolAcceptFees (fees?: BigNumber): Promise { + const { txid, vout } = await container.fundAddress(input.bech32, 10) + const inputs = [{ txid: txid, vout: vout }] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('9.5') -describe('lifecycle stateless unspent, self tracked', () => { - const bech32 = 'bcrt1qg6qmejx224dqwl7zl5gwr8mn57fwx5lppmqen6' - const privKey = 'cVEEgcoxP7cPB23ZM1Y2k33z66rABjbtFZ6AVRKai1XiamZ3Fe5z' + const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) + const signed = await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey]) + // 32 + 68 + 10 = 110 bytes + // 1000/100 * 0.5 = 4.54545 + return await client.rawtx.testMempoolAccept(signed.hex, fees) + } + + it('testMempoolAccept() should succeed with any fees', async () => { + const result = await testMempoolAcceptFees() + expect(result.allowed).toBe(true) + }) + + it('testMempoolAccept() should succeed with high fees rate', async () => { + const result = await testMempoolAcceptFees(new BigNumber('10.0')) + expect(result.allowed).toBe(true) + }) + + it('testMempoolAccept() should succeed just above expected fees', async () => { + const result = await testMempoolAcceptFees(new BigNumber('4.6')) + expect(result.allowed).toBe(true) + }) + + it('testMempoolAccept() should fail with low fee rate', async () => { + const result = await testMempoolAcceptFees(new BigNumber('4.5')) + expect(result.allowed).toBe(false) + expect(result['reject-reason']).toBe('256: absurdly-high-fee') + }) + + it('testMempoolAccept() should fail with extreme low fee rate', async () => { + const result = await testMempoolAcceptFees(new BigNumber('0.01')) + expect(result.allowed).toBe(false) + expect(result['reject-reason']).toBe('256: absurdly-high-fee') + }) }) +describe('sendRawTransaction', () => { + it('should sendRawTransaction() and get rawtx and wait confirmations', async () => { + const inputs = [ + await container.fundAddress(input.bech32, 10) + ] + + const outputs: CreateRawTxOut = {} + outputs[output.bech32] = new BigNumber('9.9') + const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) + const signed = await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey]) + const txid = await client.rawtx.sendRawTransaction(signed.hex) + + const tx = await container.call('getrawtransaction', [txid, true]) + expect(tx.txid).toBe(txid) + }) +}) diff --git a/packages/jellyfish-api-core/src/category/rawtx.ts b/packages/jellyfish-api-core/src/category/rawtx.ts index 691742b751..fcfcde770c 100644 --- a/packages/jellyfish-api-core/src/category/rawtx.ts +++ b/packages/jellyfish-api-core/src/category/rawtx.ts @@ -19,26 +19,22 @@ export class RawTx { this.client = client } - private static asHex (rawTx: string | Buffer): string { - return Buffer.isBuffer(rawTx) ? rawTx.toString('hex') : rawTx - } - /** * Create a transaction spending the given inputs and creating new outputs. * Returns hex-encoded raw transaction. * Note that the transaction's inputs are not signed, and * it is not stored in the wallet or transmitted to the network. * - * @param inputs {CreateRawTxIn[]} array of inputs - * @param outputs {CreateRawTxOut[]} array with outputs - * @param options {CreateRawTxOptions} - * @param options.locktime {number} Non-0 value also locktime-activates inputs - * @param options.replaceable {boolean} Marks this transaction as BIP125-replaceable - * @return {Promise} hex string of the transaction (Little Endian) + * @param {CreateRawTxIn[]} inputs array of inputs + * @param {CreateRawTxOut[]} outputs array with outputs + * @param {CreateRawTxOptions} options + * @param {number} options.locktime Non-0 value also locktime-activates inputs + * @param {boolean} options.replaceable Marks this transaction as BIP125-replaceable + * @return {Promise} hex string of the transaction */ async createRawTransaction ( inputs: CreateRawTxIn[], - outputs: CreateRawTxOut[], + outputs: CreateRawTxOut, options: CreateRawTxOptions = {} ): Promise { const { locktime = 0, replaceable = false } = options @@ -55,23 +51,22 @@ export class RawTx { * The third optional argument (may be null) is an array of previous transaction outputs that * this transaction depends on but may not yet be in the block chain. * - * @param rawTx {string|Buffer} unsigned raw transaction - * @param privKeys {string[] | Buffer[]} array of base58-encoded private keys for signing (WIF) - * @param prevTxs {SignRawTxWithKeyPrevTx[]} array of previous dependent transaction outputs - * @param options {SignRawTxWithKeyOptions} - * @param options.sigHashType {SigHashType} The signature hash type to use + * @param {string} rawTx unsigned raw transaction + * @param {string[]} privKeys array of base58-encoded private keys for signing (WIF) + * @param {SignRawTxWithKeyPrevTx[]} prevTxs array of previous dependent transaction outputs + * @param {SignRawTxWithKeyOptions} options + * @param {SigHashType} options.sigHashType The signature hash type to use * @return {Promise} */ async signRawTransactionWithKey ( - rawTx: string | Buffer, + rawTx: string, privKeys: string[], - prevTxs: SignRawTxWithKeyPrevTx[], + prevTxs?: SignRawTxWithKeyPrevTx[], options: SignRawTxWithKeyOptions = {} ): Promise { - const hex = RawTx.asHex(rawTx) const { sigHashType = SigHashType.ALL } = options return await this.client.call('signrawtransactionwithkey', [ - hex, privKeys, prevTxs, sigHashType + rawTx, privKeys, prevTxs, sigHashType ], 'number') } @@ -79,17 +74,16 @@ export class RawTx { * Returns result of mempool acceptance tests indicating if raw transaction would be accepted by mempool. * This checks if the transaction violates the consensus or policy rules. * - * @param signedTx {string|Buffer} signed raw transaction - * @param maxFeeRate {BigNumber} Reject transactions whose fee rate is higher than the specified value. + * @param {string} signedTx signed raw transaction + * @param {BigNumber} maxFeeRate Reject transactions whose fee rate is higher than the specified value. in DFI/kB * @return {Promise} transaction mempool accept result * @see sendRawTransaction * @see createRawTransaction * @see signRawTransactionWithKey */ - async testMempoolAccept (signedTx: string | Buffer, maxFeeRate: BigNumber = new BigNumber('0')): Promise { - const hex = RawTx.asHex(signedTx) + async testMempoolAccept (signedTx: string, maxFeeRate: BigNumber = new BigNumber('0')): Promise { const results: TestMempoolAcceptResult[] = await this.client.call('testmempoolaccept', [ - [hex], maxFeeRate + [signedTx], maxFeeRate ], 'number') return results[0] } @@ -100,17 +94,16 @@ export class RawTx { * for manual rebroadcast may degrade privacy by leaking the transaction's origin, as * nodes will normally not rebroadcast non-wallet transactions already in their mempool. * - * @param signedTx {string|Buffer} signed raw transaction - * @param maxFeeRate {BigNumber} Reject transactions whose fee rate is higher than the specified value. + * @param {string} signedTx signed raw transaction + * @param {BigNumber} maxFeeRate Reject transactions whose fee rate is higher than the specified value. in DFI/kB * @return {Promise} transaction hash in hex * @see testMempoolAccept * @see createRawTransaction * @see signRawTransactionWithKey */ - async sendRawTransaction (signedTx: string | Buffer, maxFeeRate: BigNumber = new BigNumber('0')): Promise { - const hex = RawTx.asHex(signedTx) + async sendRawTransaction (signedTx: string, maxFeeRate: BigNumber = new BigNumber('0')): Promise { return await this.client.call('sendrawtransaction', [ - hex, maxFeeRate + signedTx, maxFeeRate ], 'number') } } @@ -144,13 +137,17 @@ export interface SignRawTxWithKeyPrevTx { */ scriptPubKey: string /** - * required for P2SH or P2WSH + * Required for P2SH or P2WSH */ redeemScript?: string /** - * The amount spent + * Required for P2WSH or P2SH-P2WSH witness script + */ + witnessScript?: string + /** + * Required for segwit inputs */ - amount: BigNumber + amount?: BigNumber } export interface SignRawTxWithKeyOptions { @@ -205,5 +202,5 @@ export interface TestMempoolAcceptResult { /** * Rejection string, only present when 'allowed' is false */ - ['reject-reason']?: string + 'reject-reason'?: string } From a66fcf69b00bbf7ce010e8fde5012dc6d124671b Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 11:46:31 +0800 Subject: [PATCH 3/5] added docs --- .../jellyfish-api-core/src/category/rawtx.ts | 24 ++-- website/docs/jellyfish/api/rawtx.md | 121 ++++++++++++++++++ website/sidebars.js | 1 + 3 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 website/docs/jellyfish/api/rawtx.md diff --git a/packages/jellyfish-api-core/src/category/rawtx.ts b/packages/jellyfish-api-core/src/category/rawtx.ts index fcfcde770c..61703aa522 100644 --- a/packages/jellyfish-api-core/src/category/rawtx.ts +++ b/packages/jellyfish-api-core/src/category/rawtx.ts @@ -20,10 +20,8 @@ export class RawTx { } /** - * Create a transaction spending the given inputs and creating new outputs. - * Returns hex-encoded raw transaction. - * Note that the transaction's inputs are not signed, and - * it is not stored in the wallet or transmitted to the network. + * Create a transaction spending the given inputs and creating new outputs that returns a hex-encoded raw transaction. + * Note that the transaction's inputs are not signed, and it is not stored in the wallet or transmitted to the network. * * @param {CreateRawTxIn[]} inputs array of inputs * @param {CreateRawTxOut[]} outputs array with outputs @@ -45,11 +43,9 @@ export class RawTx { } /** - * Sign inputs for raw transaction (serialized, hex-encoded). - * The second argument is an array of base58-encoded private - * keys that will be the only keys used to sign the transaction. - * The third optional argument (may be null) is an array of previous transaction outputs that - * this transaction depends on but may not yet be in the block chain. + * Sign inputs for raw transaction (serialized, hex-encoded), Providing an array of base58-encoded private keys that + * will be the keys used to sign the transaction. An optional array of previous transaction outputs that this + * transaction depends on but may not yet be in the blockchain. * * @param {string} rawTx unsigned raw transaction * @param {string[]} privKeys array of base58-encoded private keys for signing (WIF) @@ -72,7 +68,8 @@ export class RawTx { /** * Returns result of mempool acceptance tests indicating if raw transaction would be accepted by mempool. - * This checks if the transaction violates the consensus or policy rules. + * This checks if the transaction violates the consensus or policy rules. The fee rate is expressed is DFI/kB, + * using the vSize of the transaction. * * @param {string} signedTx signed raw transaction * @param {BigNumber} maxFeeRate Reject transactions whose fee rate is higher than the specified value. in DFI/kB @@ -89,10 +86,9 @@ export class RawTx { } /** - * Submit a raw transaction (serialized, hex-encoded) to connected node and network. - * Note that the transaction will be sent unconditionally to all peers, so using this - * for manual rebroadcast may degrade privacy by leaking the transaction's origin, as - * nodes will normally not rebroadcast non-wallet transactions already in their mempool. + * Submit a raw transaction (serialized, hex-encoded) to the connected node and network. The transaction will be sent + * unconditionally to all peers, so using this for manual rebroadcast may degrade privacy by leaking the transaction's + * origin, as nodes will normally not rebroadcast non-wallet transactions already in their mempool. * * @param {string} signedTx signed raw transaction * @param {BigNumber} maxFeeRate Reject transactions whose fee rate is higher than the specified value. in DFI/kB diff --git a/website/docs/jellyfish/api/rawtx.md b/website/docs/jellyfish/api/rawtx.md new file mode 100644 index 0000000000..1bbaa0c69d --- /dev/null +++ b/website/docs/jellyfish/api/rawtx.md @@ -0,0 +1,121 @@ +--- +id: rawtx +title: Raw Transaction API +sidebar_label: RawTx API +slug: /jellyfish/api/rawtx +--- + +```js +import {Client} from '@defichain/jellyfish' +const client = new Client() + +// Using client.rawtx. +const something = await client.rawtx.method() +``` + +## createRawTransaction + +Create a transaction spending the given inputs and creating new outputs that returns a hex-encoded raw transaction. +Note that the transaction's inputs are not signed, and it is not stored in the wallet or transmitted to the network. + +```ts title="client.rawtx.createRawTransaction()" +interface rawtx { + createRawTransaction ( + inputs: CreateRawTxIn[], + outputs: CreateRawTxOut, + options: CreateRawTxOptions = {} + ): Promise +} + +interface CreateRawTxOptions { + locktime?: number + replaceable?: boolean +} + +interface CreateRawTxIn { + txid: string + vout: number + sequence?: number +} + +interface CreateRawTxOut { + [address: string]: BigNumber +} +``` + + +## signRawTransactionWithKey + +Sign inputs for raw transaction (serialized, hex-encoded), Providing an array of base58-encoded private keys that will +be the keys used to sign the transaction. An optional array of previous transaction outputs that this transaction +depends on but may not yet be in the blockchain. + +```ts title="client.rawtx.signRawTransactionWithKey()" +interface rawtx { + signRawTransactionWithKey ( + rawTx: string, + privKeys: string[], + prevTxs?: SignRawTxWithKeyPrevTx[], + options: SignRawTxWithKeyOptions = {} + ): Promise +} + +interface SignRawTxWithKeyPrevTx { + txid: string + vout: number + scriptPubKey: string + redeemScript?: string + witnessScript?: string + amount?: BigNumber +} + +interface SignRawTxWithKeyOptions { + sigHashType?: SigHashType +} + +enum SigHashType { + ALL = 'ALL', + NONE = 'NONE', + SINGLE = 'SINGLE', + ALL_ANYONECANPAY = 'ALL|ANYONECANPAY', + NONE_ANYONECANPAY = 'NONE|ANYONECANPAY', + SINGLE_ANYONECANPAY = 'SINGLE|ANYONECANPAY', +} +``` + +## testMempoolAccept + +Returns result of mempool acceptance tests indicating if raw transaction would be accepted by mempool. +This checks if the transaction violates the consensus or policy rules. The fee rate is expressed is DFI/kB, using the +vSize of the transaction. + +```ts title="client.rawtx.testMempoolAccept()" +interface rawtx { + testMempoolAccept ( + signedTx: string, + maxFeeRate: BigNumber = new BigNumber('0') + ): Promise +} + +interface TestMempoolAcceptResult { + txid: string + allowed: boolean + 'reject-reason'?: string +} + +``` + +## sendRawTransaction + +Submit a raw transaction (serialized, hex-encoded) to the connected node and network. The transaction will be sent +unconditionally to all peers, so using this for manual rebroadcast may degrade privacy by leaking the transaction's +origin, as nodes will normally not rebroadcast non-wallet transactions already in their mempool. + +```ts title="client.rawtx.sendRawTransaction()" +interface rawtx { + sendRawTransaction ( + signedTx: string, + maxFeeRate: BigNumber = new BigNumber('0') + ): Promise +} +``` diff --git a/website/sidebars.js b/website/sidebars.js index d0ee0e9559..f7ac3e13df 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -10,6 +10,7 @@ module.exports = { items: [ 'jellyfish/api/blockchain', 'jellyfish/api/mining', + 'jellyfish/api/rawtx', 'jellyfish/api/wallet' ] } From b88cbe908d83ee55f0c23cb456fbe608db818be9 Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 19:39:34 +0800 Subject: [PATCH 4/5] moved optional prevTxs to options as suggested in the guidelines --- .../__tests__/category/rawtx.test.ts | 2 +- .../jellyfish-api-core/src/category/rawtx.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts index e9148cb883..57a8d51549 100644 --- a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts @@ -152,7 +152,7 @@ describe('signRawTransactionWithKey', () => { outputs[input.bech32] = new BigNumber('4.9') const unsigned = await client.rawtx.createRawTransaction(inputs, outputs) - return await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey], [], { + return await client.rawtx.signRawTransactionWithKey(unsigned, [input.privKey], { sigHashType: type }) } diff --git a/packages/jellyfish-api-core/src/category/rawtx.ts b/packages/jellyfish-api-core/src/category/rawtx.ts index 61703aa522..bf1aedf066 100644 --- a/packages/jellyfish-api-core/src/category/rawtx.ts +++ b/packages/jellyfish-api-core/src/category/rawtx.ts @@ -49,20 +49,19 @@ export class RawTx { * * @param {string} rawTx unsigned raw transaction * @param {string[]} privKeys array of base58-encoded private keys for signing (WIF) - * @param {SignRawTxWithKeyPrevTx[]} prevTxs array of previous dependent transaction outputs * @param {SignRawTxWithKeyOptions} options - * @param {SigHashType} options.sigHashType The signature hash type to use + * @param {SigHashType} options.sigHashType the signature hash type to use + * @param {SignRawTxWithKeyPrevTx[]} options.prevTxs array of previous dependent transaction outputs * @return {Promise} */ async signRawTransactionWithKey ( rawTx: string, privKeys: string[], - prevTxs?: SignRawTxWithKeyPrevTx[], options: SignRawTxWithKeyOptions = {} ): Promise { const { sigHashType = SigHashType.ALL } = options return await this.client.call('signrawtransactionwithkey', [ - rawTx, privKeys, prevTxs, sigHashType + rawTx, privKeys, options.prevTxs, sigHashType ], 'number') } @@ -119,6 +118,11 @@ export interface CreateRawTxOut { [address: string]: BigNumber } +export interface SignRawTxWithKeyOptions { + prevTxs?: SignRawTxWithKeyPrevTx[] + sigHashType?: SigHashType +} + export interface SignRawTxWithKeyPrevTx { /** * The transaction id @@ -146,10 +150,6 @@ export interface SignRawTxWithKeyPrevTx { amount?: BigNumber } -export interface SignRawTxWithKeyOptions { - sigHashType?: SigHashType -} - export interface SignRawTxWithKeyResult { /** * The hex-encoded raw transaction with signature(s) From cee5030bef106ac432613a283948ee15d975aa0a Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 20:27:29 +0800 Subject: [PATCH 5/5] fix docs and refactor --- packages/jellyfish-api-core/src/category/rawtx.ts | 15 +++++++++++---- website/docs/jellyfish/api/rawtx.md | 10 +++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/jellyfish-api-core/src/category/rawtx.ts b/packages/jellyfish-api-core/src/category/rawtx.ts index bf1aedf066..232864857a 100644 --- a/packages/jellyfish-api-core/src/category/rawtx.ts +++ b/packages/jellyfish-api-core/src/category/rawtx.ts @@ -59,9 +59,10 @@ export class RawTx { privKeys: string[], options: SignRawTxWithKeyOptions = {} ): Promise { - const { sigHashType = SigHashType.ALL } = options + const { prevTxs = [], sigHashType = SigHashType.ALL } = options + return await this.client.call('signrawtransactionwithkey', [ - rawTx, privKeys, options.prevTxs, sigHashType + rawTx, privKeys, prevTxs, sigHashType ], 'number') } @@ -77,7 +78,10 @@ export class RawTx { * @see createRawTransaction * @see signRawTransactionWithKey */ - async testMempoolAccept (signedTx: string, maxFeeRate: BigNumber = new BigNumber('0')): Promise { + async testMempoolAccept ( + signedTx: string, + maxFeeRate: BigNumber = new BigNumber('0') + ): Promise { const results: TestMempoolAcceptResult[] = await this.client.call('testmempoolaccept', [ [signedTx], maxFeeRate ], 'number') @@ -96,7 +100,10 @@ export class RawTx { * @see createRawTransaction * @see signRawTransactionWithKey */ - async sendRawTransaction (signedTx: string, maxFeeRate: BigNumber = new BigNumber('0')): Promise { + async sendRawTransaction ( + signedTx: string, maxFeeRate: + BigNumber = new BigNumber('0') + ): Promise { return await this.client.call('sendrawtransaction', [ signedTx, maxFeeRate ], 'number') diff --git a/website/docs/jellyfish/api/rawtx.md b/website/docs/jellyfish/api/rawtx.md index 1bbaa0c69d..504bbe564d 100644 --- a/website/docs/jellyfish/api/rawtx.md +++ b/website/docs/jellyfish/api/rawtx.md @@ -55,11 +55,15 @@ interface rawtx { signRawTransactionWithKey ( rawTx: string, privKeys: string[], - prevTxs?: SignRawTxWithKeyPrevTx[], options: SignRawTxWithKeyOptions = {} ): Promise } +interface SignRawTxWithKeyOptions { + prevTxs?: SignRawTxWithKeyPrevTx[] + sigHashType?: SigHashType +} + interface SignRawTxWithKeyPrevTx { txid: string vout: number @@ -69,10 +73,6 @@ interface SignRawTxWithKeyPrevTx { amount?: BigNumber } -interface SignRawTxWithKeyOptions { - sigHashType?: SigHashType -} - enum SigHashType { ALL = 'ALL', NONE = 'NONE',