diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_burn_token.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_burn_token.test.ts index d59a18f54e..3c6e3ac572 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_burn_token.test.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_burn_token.test.ts @@ -65,7 +65,7 @@ describe('burnToken', () => { await setupGovs() - // Fund 10 DFI UTXO + // Fund 100 DFI UTXO await fundEllipticPair(testing.container, providers.ellipticPair, 100) await providers.setupMocks() diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_mint_token.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_mint_token.test.ts new file mode 100644 index 0000000000..2c19c83ac2 --- /dev/null +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_token_mint_token.test.ts @@ -0,0 +1,382 @@ +import { StartFlags } from '@defichain/testcontainers' +import { getProviders, MockProviders } from '../provider.mock' +import { P2WPKHTransactionBuilder } from '../../src' +import { fundEllipticPair, sendTransaction } from '../test.utils' +import BigNumber from 'bignumber.js' +import { TestingGroup } from '@defichain/jellyfish-testing' +import { RegTest } from '@defichain/jellyfish-network' +import { P2WPKH } from '@defichain/jellyfish-address' +import { OP_CODES, TokenMint } from '@defichain/jellyfish-transaction' +import { Bech32, WIF } from '@defichain/jellyfish-crypto' + +const attributeKey = 'ATTRIBUTES' +const symbolDBTC = 'BTC' + +describe('Consortium', () => { + const tGroup = TestingGroup.create(3) + const alice = tGroup.get(0) + const bob = tGroup.get(1) + const charlie = tGroup.get(2) + let account0: string + let idBTC: string, idDOGE: string + const symbolBTC = 'BTC' + const symbolDOGE = 'DOGE' + + let aProviders: MockProviders, bProviders: MockProviders + let aBuilder: P2WPKHTransactionBuilder, bBuilder: P2WPKHTransactionBuilder + + const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] + + const consortiumMemberA = { + bech32: 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn', + privKey: 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' + } + const consortiumMemberB = { + bech32: 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7', + privKey: 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' + } + + beforeEach(async () => { + await tGroup.start({ startFlags }) + await alice.container.waitForWalletCoinbaseMaturity() + + account0 = await alice.generateAddress() + + await alice.token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: account0 + }) + await alice.generate(1) + + await alice.token.create({ + symbol: symbolDOGE, + name: symbolDOGE, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: account0 + }) + await alice.generate(1) + + idBTC = await alice.token.getTokenId(symbolDBTC) + idDOGE = await alice.token.getTokenId(symbolDOGE) + + aProviders = await getProviders(alice.container) + aProviders.setEllipticPair(WIF.asEllipticPair(consortiumMemberA.privKey)) + aBuilder = new P2WPKHTransactionBuilder(aProviders.fee, aProviders.prevout, aProviders.elliptic, RegTest) + + await aProviders.setupMocks() + await fundEllipticPair(alice.container, aProviders.ellipticPair, 100) + + bProviders = await getProviders(bob.container) + bProviders.setEllipticPair(WIF.asEllipticPair(consortiumMemberB.privKey)) + bBuilder = new P2WPKHTransactionBuilder(bProviders.fee, bProviders.prevout, bProviders.elliptic, RegTest) + + await bProviders.setupMocks() + await fundEllipticPair(bob.container, bProviders.ellipticPair, 100) + + await setupGovs() + }) + + afterEach(async () => { + await tGroup.stop() + }) + + async function setupGovs (): Promise { + await alice.rpc.masternode.setGov({ + [attributeKey]: + { + // Enable consortium + 'v0/params/feature/consortium': 'true', + 'v0/params/feature/mint-tokens-to-address': 'true', + + // Set a consortium global limit for dBTC + [`v0/consortium/${idBTC}/mint_limit`]: '50', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '40', + + // Set a consortium member for dBTC + [`v0/consortium/${idBTC}/members`]: { + '01': { + name: 'Waves HQ', + ownerAddress: consortiumMemberA.bech32, + backingId: 'backing_address_btc_1_c', + mintLimitDaily: '40.00000000', + mintLimit: '50.00000000' + }, + '02': { + name: 'Alexandria', + ownerAddress: consortiumMemberB.bech32, + backingId: 'backing_address_btc_2_c', + mintLimitDaily: '40.00000000', + mintLimit: '50.00000000' + } + } + } + }) + await alice.generate(1) + await tGroup.waitForSync() + } + + it('should throw an error if foundation or consortium member authorization is not present', async () => { + await alice.rpc.masternode.setGov({ + [attributeKey]: + { + 'v0/params/feature/consortium': 'true' + } + }) + await alice.generate(1) + await tGroup.waitForSync() + + const cProviders = await getProviders(alice.container) + const cBuilder = new P2WPKHTransactionBuilder(cProviders.fee, cProviders.prevout, cProviders.elliptic, RegTest) + + await charlie.container.waitForWalletCoinbaseMaturity() + await charlie.token.dfi({ address: await charlie.generateAddress(), amount: 12 }) + await charlie.generate(1) + + await fundEllipticPair(charlie.container, cProviders.ellipticPair, 10) + await tGroup.waitForSync() + await cProviders.setupMocks() + + const script = await cProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 0.5 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(0.5) }], + to: { + stack: [] + } + } + + const txn = await cBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(charlie.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: You are not a foundation or consortium member and cannot mint this token! (code 16)', code: -26") + }) + + it('should throw an error if the token is not specified in governance vars', async () => { + await setupGovs() + + const script = await aProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 1 DOGE + balances: [{ token: Number(idDOGE), amount: new BigNumber(1) }], + to: { + stack: [] + } + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(alice.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: You are not a foundation member or token owner and cannot mint this token! (code 16)', code: -26") + }) + + it('should not mintTokens for non-existent token', async () => { + await setupGovs() + + const script = await aProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint non-existent token + balances: [{ token: 22, amount: new BigNumber(1) }], + to: { + stack: [] + } + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(alice.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: token 22 does not exist! (code 16)', code: -26") + }) + + it('should throw an error if member daily mint limit exceeds', async () => { + await setupGovs() + + const script = await aProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 41 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(41) }], + to: { + stack: [] + } + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(alice.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: You will exceed your daily mint limit for BTC token by minting this amount (code 16)', code: -26") + }) + + it('should throw an error if member maximum mint limit exceeds', async () => { + await setupGovs() + + const script = await aProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 51 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(51) }], + to: { + stack: [] + } + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(alice.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: You will exceed your maximum mint limit for BTC token by minting this amount! (code 16)', code: -26") + }) + + it('should throw an error if global daily mint limit exceeds', async () => { + await setupGovs() + + // Hit global daily mint limit + await alice.rpc.token.mintTokens({ amounts: [`40.0000000@${symbolBTC}`] }) + await alice.generate(1) + await tGroup.waitForSync() + + const script = await bProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 1 BTC to hit global daily mint lint + balances: [{ token: Number(idBTC), amount: new BigNumber(1) }], + to: { + stack: [] + } + } + + const txn = await bBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(bob.container, txn) + + await expect(promise).rejects.toThrow("DeFiDRpcError: 'MintTokenTx: You will exceed global daily maximum consortium mint limit for BTC token by minting this amount. (code 16)', code: -26") + }) + + it('should throw an error if global mint limit exceeds', async () => { + await setupGovs() + + // Hit global daily mint limit + await alice.rpc.token.mintTokens({ amounts: [`40.0000000@${symbolBTC}`] }) + await alice.generate(1) + await tGroup.waitForSync() + + const script = await bProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 50 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(10) }], + to: { + stack: [] + } + } + + const txn = await bBuilder.tokens.mint(tokenMint, script) + const promise = sendTransaction(bob.container, txn) + + await expect(promise).rejects.toThrow("MintTokenTx: You will exceed global daily maximum consortium mint limit for BTC token by minting this amount. (code 16)', code: -26") + }) + + it('should mintTokens', async () => { + await setupGovs() + + const script = await aProviders.elliptic.script() + const tokenMint: TokenMint = { + // Mint 11.5 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(11.5) }], + to: { + stack: [] + } + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + + // Ensure the created txn is correct + const outs = await sendTransaction(alice.container, txn) + const encoded: string = OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint).asBuffer().toString('hex') + const pubKey = await aProviders.ellipticPair.publicKey() + const address = Bech32.fromPubKey(pubKey, 'bcrt') + + expect(outs).toStrictEqual([{ + n: 0, + scriptPubKey: { + asm: expect.stringMatching(/^OP_RETURN 446654784d/), + hex: `6a${encoded}`, + type: 'nulldata' + }, + tokenId: 0, + value: 0 + }, { + n: 1, + scriptPubKey: { + addresses: [address], + asm: expect.any(String), + hex: expect.any(String), + reqSigs: 1, + type: 'witness_v0_keyhash' + }, + tokenId: 0, + value: 99.9999929 + }]) + + await alice.generate(1) + + // Verify that the amount is minted to consortium member account + const accAfter = await alice.rpc.account.getAccount(consortiumMemberA.bech32) + expect(accAfter).toStrictEqual(['11.50000000@BTC']) + + // Verify that the amount reflects to consortium member and token level attributes + const attr = (await alice.rpc.masternode.getGov(attributeKey)).ATTRIBUTES + expect(attr[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber('11.5')) + expect(attr[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual(`0/${new BigNumber(11.5).toFixed(8)}`) + expect(attr['v0/live/economy/consortium/1/minted']).toStrictEqual(new BigNumber('11.5')) + }) + + it('should allow to mint tokens to an address', async () => { + await setupGovs() + + const script = P2WPKH.fromAddress(RegTest, consortiumMemberB.bech32, P2WPKH).getScript() + const tokenMint: TokenMint = { + // Mint 9.3 BTC + balances: [{ token: Number(idBTC), amount: new BigNumber(9.3) }], + to: script + } + + const txn = await aBuilder.tokens.mint(tokenMint, script) + + // Ensure the created txn is correct + const outs = await sendTransaction(bob.container, txn) + const encoded: string = OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint).asBuffer().toString('hex') + + expect(outs).toStrictEqual([{ + n: 0, + scriptPubKey: { + asm: expect.stringMatching(/^OP_RETURN 446654784d/), + hex: `6a${encoded}`, + type: 'nulldata' + }, + tokenId: 0, + value: 0 + }, { + n: 1, + scriptPubKey: { + addresses: [consortiumMemberB.bech32], + asm: expect.any(String), + hex: expect.any(String), + reqSigs: 1, + type: 'witness_v0_keyhash' + }, + tokenId: 0, + value: 99.9999918 + }]) + await alice.generate(1) + + // Verify that the amount is minted to consortium member account + const accAfter = (await alice.rpc.account.getAccount(consortiumMemberB.bech32)) + expect(accAfter).toStrictEqual(['9.30000000@BTC']) + + // Verify that the amount reflects to consortium member and token level attributes + const attr = (await alice.rpc.masternode.getGov(attributeKey)).ATTRIBUTES + expect(attr[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber('9.3')) + expect(attr[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual(`0/${new BigNumber(9.3).toFixed(8)}`) + expect(attr['v0/live/economy/consortium/1/minted']).toStrictEqual(new BigNumber('9.3')) + }) +}) diff --git a/packages/jellyfish-transaction-builder/src/txn/txn_builder_token.ts b/packages/jellyfish-transaction-builder/src/txn/txn_builder_token.ts index 3afdf87b95..c7316cfedb 100644 --- a/packages/jellyfish-transaction-builder/src/txn/txn_builder_token.ts +++ b/packages/jellyfish-transaction-builder/src/txn/txn_builder_token.ts @@ -1,5 +1,5 @@ import { - OP_CODES, Script, TransactionSegWit, TokenBurn + OP_CODES, Script, TransactionSegWit, TokenBurn, TokenMint } from '@defichain/jellyfish-transaction' import { P2WPKHTxnBuilder } from './txn_builder' @@ -17,4 +17,18 @@ export class TxnBuilderTokens extends P2WPKHTxnBuilder { changeScript ) } + + /** + * Mint tokens + * + * @param {TokenMint} tokenMint txn to create + * @param {Script} changeScript to send unspent to after deducting the (converted + fees) + * @returns {Promise} + */ + async mint (tokenMint: TokenMint, changeScript: Script): Promise { + return await super.createDeFiTx( + OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint), + changeScript + ) + } } diff --git a/packages/jellyfish-transaction/__tests__/script/dftx/dftx_token/TokenMint.test.ts b/packages/jellyfish-transaction/__tests__/script/dftx/dftx_token/TokenMint.test.ts index 55baa7f008..f6c9c92d00 100644 --- a/packages/jellyfish-transaction/__tests__/script/dftx/dftx_token/TokenMint.test.ts +++ b/packages/jellyfish-transaction/__tests__/script/dftx/dftx_token/TokenMint.test.ts @@ -7,8 +7,9 @@ import { CTokenMint, TokenMint } from '../../../../src/script/dftx/dftx_token' it('should bi-directional buffer-object-buffer', () => { const fixtures = [ - '6a12446654784d010200000000ca9a3b00000000', - '6a12446654784d010200000000ea56fa00000000' + '6a13446654784d010200000000ca9a3b0000000000', + '6a13446654784d010200000000ea56fa0000000000', + '6a29446654784d010100000000e1f50500000000160014dd527be30bedb3de69fee5ebe32af430686cfe3f' ] fixtures.forEach(hex => { @@ -21,40 +22,100 @@ it('should bi-directional buffer-object-buffer', () => { }) }) -const header = '6a12446654784d' // OP_RETURN, PUSH_DATA(44665478, 4d) -const data = '01010000006050da6001000000' -const tokenMint: TokenMint = { - balances: [ +const tokenMintData: Array<{ header: string, data: string, tokenMint: TokenMint }> = [ + { + header: '6a13446654784d', // OP_RETURN(0x6a) (length 19 = 0x13) CDfTx.SIGNATURE(0x44665478) CTokenMint.OP_CODE(0x4d) + // TokenMint.balances(0x01010000006050da6001000000) + // TokenMint.to[LE](00) + data: '01010000006050da600100000000', + tokenMint: { + balances: [ + { + token: 1, + amount: new BigNumber('59.19887456') + } + ], + to: { + stack: [] + } + } + }, + { + header: '6a29446654784d', // OP_RETURN(0x6a) (length 69 = 0x29) CDfTx.SIGNATURE(0x44665478) CTokenMint.OP_CODE(0x4d) + // TokenMint.balances(0x010100000000e1f50500000000) + // TokenMint.to(160014ad54d71e8681e0c990349070cbd17a5c567a9b9e) + data: '010100000000e1f50500000000160014ad54d71e8681e0c990349070cbd17a5c567a9b9e', + tokenMint: { - token: 1, - amount: new BigNumber('59.19887456') + balances: [ + { + token: 1, + amount: new BigNumber('1') + } + ], + to: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA_HEX_LE('ad54d71e8681e0c990349070cbd17a5c567a9b9e') + ] + } } - ] -} + } +] -it('should craft dftx with OP_CODES._()', () => { - const stack = [ - OP_CODES.OP_RETURN, - OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint) - ] +describe.each(tokenMintData)('should craft and compose dftx', + ({ header, tokenMint, data }: { header: string, data: string, tokenMint: TokenMint }) => { + it('should craft dftx with OP_CODES._()', () => { + const stack = [ + OP_CODES.OP_RETURN, + OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint) + ] - const buffer = toBuffer(stack) - expect(buffer.toString('hex')).toStrictEqual(header + data) -}) + const buffer = toBuffer(stack) + expect(buffer.toString('hex')).toStrictEqual(header + data) + }) -describe('Composable', () => { - it('should compose from buffer to composable', () => { - const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex')) - const composable = new CTokenMint(buffer) + describe('Composable', () => { + it('should compose from buffer to composable', () => { + const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex')) + const composable = new CTokenMint(buffer) - expect(composable.toObject()).toStrictEqual(tokenMint) - }) + expect(composable.toObject()).toStrictEqual(tokenMint) + }) + + it('should compose from composable to buffer', () => { + const composable = new CTokenMint(tokenMint) + const buffer = new SmartBuffer() + composable.toBuffer(buffer) - it('should compose from composable to buffer', () => { - const composable = new CTokenMint(tokenMint) - const buffer = new SmartBuffer() - composable.toBuffer(buffer) + expect(buffer.toBuffer().toString('hex')).toStrictEqual(data) + }) + }) - expect(buffer.toBuffer().toString('hex')).toStrictEqual(data) + it('should craft dftx with OP_CODES._()', () => { + const stack = [ + OP_CODES.OP_RETURN, + OP_CODES.OP_DEFI_TX_TOKEN_MINT(tokenMint) + ] + + const buffer = toBuffer(stack) + expect(buffer.toString('hex')).toStrictEqual(header + data) + }) + + describe('Composable', () => { + it('should compose from buffer to composable', () => { + const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex')) + const composable = new CTokenMint(buffer) + + expect(composable.toObject()).toStrictEqual(tokenMint) + }) + + it('should compose from composable to buffer', () => { + const composable = new CTokenMint(tokenMint) + const buffer = new SmartBuffer() + composable.toBuffer(buffer) + + expect(buffer.toBuffer().toString('hex')).toStrictEqual(data) + }) + }) }) -}) diff --git a/packages/jellyfish-transaction/src/script/dftx/dftx_token.ts b/packages/jellyfish-transaction/src/script/dftx/dftx_token.ts index 44f2309ab4..c464cc8d20 100644 --- a/packages/jellyfish-transaction/src/script/dftx/dftx_token.ts +++ b/packages/jellyfish-transaction/src/script/dftx/dftx_token.ts @@ -9,6 +9,7 @@ import BigNumber from 'bignumber.js' */ export interface TokenMint { balances: TokenBalanceUInt32[] // ----------| c = VarUInt{1-9 bytes}, + c x TokenBalance + to: Script } /** @@ -21,7 +22,8 @@ export class CTokenMint extends ComposableBuffer { composers (tm: TokenMint): BufferComposer[] { return [ - ComposableBuffer.compactSizeArray(() => tm.balances, v => tm.balances = v, v => new CTokenBalance(v)) + ComposableBuffer.compactSizeArray(() => tm.balances, v => tm.balances = v, v => new CTokenBalance(v)), + ComposableBuffer.single