diff --git a/packages/jellyfish-transaction/__tests__/script/defi/dftx_governance/SetGovernance.test.ts b/packages/jellyfish-transaction/__tests__/script/defi/dftx_governance/SetGovernance.test.ts new file mode 100644 index 0000000000..2e662e0ca0 --- /dev/null +++ b/packages/jellyfish-transaction/__tests__/script/defi/dftx_governance/SetGovernance.test.ts @@ -0,0 +1,192 @@ +import { SmartBuffer } from 'smart-buffer' +import { + CSetGovernance, SetGovernance +} from '../../../../src/script/defi/dftx_governance' +import { OP_CODES } from '../../../../src/script' +import { toBuffer, toOPCodes } from '../../../../src/script/_buffer' +import { OP_DEFI_TX } from '../../../../src/script/defi' +import { BigNumber } from '@defichain/jellyfish-json' + +it('should bi-directional buffer-object-buffer', () => { + const fixtures = [ + // LP_SPLITS only + '6a284466547847094c505f53504c4954530202000000809698000000000004000000804a5d0500000000', + // LP_DAILY_DFI_REWARD only + '6a214466547847134c505f4441494c595f4446495f52455741524400204aa9d1010000', + // LP_SPLITS and LP_DAILY_DFI_REWARD + '6a444466547847094c505f53504c495453020200000080c3c9010000000004000000801d2c0400000000134c505f4441494c595f4446495f524557415244c01c3d0900000000', + '6a444466547847094c505f53504c4954530202000000809698000000000004000000804a5d0500000000134c505f4441494c595f4446495f52455741524400204aa9d1010000' + ] + + fixtures.forEach(hex => { + const stack = toOPCodes( + SmartBuffer.fromBuffer(Buffer.from(hex, 'hex')) + ) + + const buffer = toBuffer(stack) + expect(buffer.toString('hex')).toBe(hex) + expect((stack[1] as OP_DEFI_TX).tx.type).toBe(0x47) + }) +}) + +describe('multiple variable', () => { + /** + * this test data is created by: + * createToken() // id = 1 + * createPoolPair() // id = 2 + * createToken() // id = 3 + * createPoolPair() // id = 4 + * await container.call('setgov', [{ + * LP_SPLITS: { + * 2: 0.3, + * 4: 0.7 + * }, + * LP_DAILY_DFI_REWARD: 1.55 + * }]) + */ + const header = '6a444466547847' // OP_RETURN, PUSH_DATA(44665478, 47) + const data = '094c505f53504c495453020200000080c3c9010000000004000000801d2c0400000000134c505f4441494c595f4446495f524557415244c01c3d0900000000' + const setGovernance: SetGovernance = { + governanceVars: [ + { + key: 'LP_SPLITS', + value: [ + { + tokenId: 2, + value: new BigNumber(0.3) + }, + { + tokenId: 4, + value: new BigNumber(0.7) + } + ] + }, + { + key: 'LP_DAILY_DFI_REWARD', + value: new BigNumber(1.55) + } + ] + } + + it('should craft dftx with OP_CODES._()', () => { + const stack = [ + OP_CODES.OP_RETURN, + OP_CODES.OP_DEFI_TX_SET_GOVERNANCE(setGovernance) + ] + + const buffer = toBuffer(stack) + expect(buffer.toString('hex')).toBe(header + data) + }) + + describe('Composable', () => { + it('should compose from buffer to composable', () => { + const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex')) + const composable = new CSetGovernance(buffer) + + expect(composable.toObject()).toEqual(setGovernance) + }) + + it('should compose from composable to buffer', () => { + const composable = new CSetGovernance(setGovernance) + const buffer = new SmartBuffer() + composable.toBuffer(buffer) + + expect(buffer.toBuffer().toString('hex')).toEqual(data) + }) + }) +}) + +describe('single variable', () => { + /** + * this test data is created by: + * createToken() // id = 1 + * createPoolPair() // id = 2 + * createToken() // id = 3 + * createPoolPair() // id = 4 + * await container.call('setgov', [{ + * LP_SPLITS: { + * 2: 0.3, + * 4: 0.7 + * } + * }]) + */ + const header = '6a284466547847' // OP_RETURN, PUSH_DATA(44665478, 47) + const data = '094c505f53504c495453020200000080c3c9010000000004000000801d2c0400000000' + const setGovernance: SetGovernance = { + governanceVars: [ + { + key: 'LP_SPLITS', + value: [ + { + tokenId: 2, + value: new BigNumber(0.3) + }, + { + tokenId: 4, + value: new BigNumber(0.7) + } + ] + } + ] + } + + it('should craft dftx with OP_CODES._()', () => { + const stack = [ + OP_CODES.OP_RETURN, + OP_CODES.OP_DEFI_TX_SET_GOVERNANCE(setGovernance) + ] + + const buffer = toBuffer(stack) + expect(buffer.toString('hex')).toBe(header + data) + }) + + describe('Composable', () => { + it('should compose from buffer to composable', () => { + const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex')) + const composable = new CSetGovernance(buffer) + + expect(composable.toObject()).toEqual(setGovernance) + }) + + it('should compose from composable to buffer', () => { + const composable = new CSetGovernance(setGovernance) + const buffer = new SmartBuffer() + composable.toBuffer(buffer) + + expect(buffer.toBuffer().toString('hex')).toEqual(data) + }) + }) +}) + +describe('Unmapped Governance Variable handling', () => { + const setGovernance: SetGovernance = { + governanceVars: [ + { + key: 'LP_DAILY_DFI_REWARD', + value: new BigNumber(1.55) + }, + { + key: 'FOO', + value: '0123456789abcdef' + } + ] + } + + const lpRewards = '134c505f4441494c595f4446495f524557415244c01c3d0900000000' + const fooBaz = '03464f4f0123456789abcdef' // [0x03 FOO {raw hex}] + + it('should compose from buffer to composable', () => { + const buffer = SmartBuffer.fromBuffer(Buffer.from(lpRewards + fooBaz, 'hex')) + const composable = new CSetGovernance(buffer) + + expect(composable.toObject()).toEqual(setGovernance) + }) + + it('should compose from composable to buffer', () => { + const composable = new CSetGovernance(setGovernance) + const buffer = new SmartBuffer() + composable.toBuffer(buffer) + + expect(buffer.toBuffer().toString('hex')).toEqual(lpRewards + fooBaz) + }) +}) diff --git a/packages/jellyfish-transaction/src/script/defi/dftx.ts b/packages/jellyfish-transaction/src/script/defi/dftx.ts index 3ec5979f22..2475a72dc5 100644 --- a/packages/jellyfish-transaction/src/script/defi/dftx.ts +++ b/packages/jellyfish-transaction/src/script/defi/dftx.ts @@ -28,6 +28,7 @@ import { SetOracleData } from './dftx_oracles' import { CDeFiOpUnmapped, DeFiOpUnmapped } from './dftx_unmapped' +import { CSetGovernance, SetGovernance } from './dftx_governance' // Disabling no-return-assign makes the code cleaner with the setter and getter */ /* eslint-disable no-return-assign */ @@ -136,6 +137,8 @@ export class CDfTx extends ComposableBuffer> { return compose(CCreateMasterNode.OP_NAME, d => new CCreateMasterNode(d)) case CResignMasterNode.OP_CODE: return compose(CResignMasterNode.OP_NAME, d => new CResignMasterNode(d)) + case CSetGovernance.OP_CODE: + return compose(CSetGovernance.OP_NAME, d => new CSetGovernance(d)) default: return compose(CDeFiOpUnmapped.OP_NAME, d => new CDeFiOpUnmapped(d)) } diff --git a/packages/jellyfish-transaction/src/script/defi/dftx_governance.ts b/packages/jellyfish-transaction/src/script/defi/dftx_governance.ts new file mode 100644 index 0000000000..72c997fca9 --- /dev/null +++ b/packages/jellyfish-transaction/src/script/defi/dftx_governance.ts @@ -0,0 +1,104 @@ +import BigNumber from 'bignumber.js' +import { SmartBuffer } from 'smart-buffer' +import { readBigNumberUInt64, writeBigNumberUInt64 } from '../../buffer/buffer_bignumber' +import { BufferComposer, ComposableBuffer } from '../../buffer/buffer_composer' + +// Disabling no-return-assign makes the code cleaner with the setter and getter */ +/* eslint-disable no-return-assign */ + +export interface LiqPoolSplit { + tokenId: number // -------------------| 4 bytes unsigned + value: BigNumber // ------------------| 8 bytes unsigned +} + +export class CLiqPoolSplit extends ComposableBuffer { + composers (lps: LiqPoolSplit): BufferComposer[] { + return [ + ComposableBuffer.uInt32(() => lps.tokenId, v => lps.tokenId = v), + ComposableBuffer.satoshiAsBigNumber(() => lps.value, v => lps.value = v) + ] + } +} + +export interface GovernanceLpDailyReward { + key: 'LP_DAILY_DFI_REWARD' // --------| 20 bytes = [0x13, Buffer.from('LP_DAILY_DFI_REWARD', ascii)] + value: BigNumber // ------------------| 8 bytes unsigned +} + +export interface GovernanceLpSplits { + key: 'LP_SPLITS' // ------------------| 10 bytes = [0x09, Buffer.from('LP_SPLITS', ascii)] + value: LiqPoolSplit[] // -------------| VarUInt{1 + n * 12 bytes} first byte = config len +} + +export interface GovernanceUnmapped { + key: string // -----------------------| VarUInt{1-9 bytes}, [length, Buffer.from(<'key here'>, ascii)] + value: string // ---------------------| Unknown length, fill in everything remained in buffer +} + +export type GovernanceVar = GovernanceLpDailyReward | GovernanceLpSplits | GovernanceUnmapped + +export class CGovernanceVar extends ComposableBuffer { + composers (gv: GovernanceVar): BufferComposer[] { + return [ + ComposableBuffer.varUIntUtf8BE(() => gv.key, v => gv.key = v), + { + fromBuffer: (buffer: SmartBuffer): void => { + if (gv.key === 'LP_DAILY_DFI_REWARD') { + gv.value = readBigNumberUInt64(buffer).div('1e8') + } else if (gv.key === 'LP_SPLITS') { + gv.value = [] + const configLen = buffer.readUInt8() + for (let i = 0; i < configLen; i++) { + gv.value.push(new CLiqPoolSplit(buffer).toObject()) + } + } else { + gv.value = buffer.readBuffer().toString('hex') + } + }, + toBuffer: (buffer: SmartBuffer): void => { + if (gv.key === 'LP_DAILY_DFI_REWARD') { + writeBigNumberUInt64(((gv.value as BigNumber)).times('1e8'), buffer) + } else if (gv.key === 'LP_SPLITS') { + const lpss = gv.value as LiqPoolSplit[] + buffer.writeUInt8(lpss.length) + lpss.forEach(lps => new CLiqPoolSplit(lps).toBuffer(buffer)) + } else { // UNMAPPED + buffer.writeBuffer(Buffer.from(gv.value as string, 'hex')) + } + } + } + ] + } +} + +export interface SetGovernance { + governanceVars: GovernanceVar[] +} + +/** + * Composable CSetGovernance, C stands for Composable. + * Immutable by design, bi-directional fromBuffer, toBuffer deep composer. + */ +export class CSetGovernance extends ComposableBuffer { + static OP_CODE = 0x47 // 'G' + static OP_NAME = 'OP_DEFI_TX_SET_GOVERNANCE' + + composers (gvs: SetGovernance): BufferComposer[] { + return [ + { + fromBuffer: (buffer: SmartBuffer): void => { + gvs.governanceVars = [] + while (buffer.remaining() > 0) { + const govVar = new CGovernanceVar(buffer) + gvs.governanceVars.push(govVar.toObject()) + } + }, + toBuffer: (buffer: SmartBuffer): void => { + gvs.governanceVars.forEach(gv => + new CGovernanceVar(gv).toBuffer(buffer) + ) + } + } + ] + } +} diff --git a/packages/jellyfish-transaction/src/script/mapping.ts b/packages/jellyfish-transaction/src/script/mapping.ts index 23630961ef..596a628ec8 100644 --- a/packages/jellyfish-transaction/src/script/mapping.ts +++ b/packages/jellyfish-transaction/src/script/mapping.ts @@ -41,6 +41,7 @@ import { } from './defi/dftx_oracles' import { CAutoAuthPrep } from './defi/dftx_misc' import { CCreateMasterNode, CreateMasterNode, CResignMasterNode, ResignMasterNode } from './defi/dftx_masternode' +import { CSetGovernance, SetGovernance } from './defi/dftx_governance' /** * @param num to map as OPCode, 1 byte long @@ -258,6 +259,14 @@ export const OP_CODES = { data: resignMasterNode }) }, + OP_DEFI_TX_SET_GOVERNANCE: (setGovernance: SetGovernance) => { + return new OP_DEFI_TX({ + signature: CDfTx.SIGNATURE, + type: CSetGovernance.OP_CODE, + name: CSetGovernance.OP_NAME, + data: setGovernance + }) + }, OP_0: new constants.OP_0(), OP_FALSE: new constants.OP_FALSE(),