Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions projects/keepkey-vault/src/bun/txbuilder/hive-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,64 @@ describe('comment_options beneficiary rules (finding #6)', () => {
.not.toThrow()
})
})

describe('phase-3: internal market (limit orders)', () => {
// Same vector as the firmware unit test Hive.LimitOrderCreateRetainsEveryDisplayedField
const CREATE = {
owner: 'alice', orderid: 42,
amount_to_sell: '1.500 HIVE', min_to_receive: '0.400 HBD',
fill_or_kill: true, expiration: 1700003600,
}
const u32le = (n: number) => { const b = Buffer.alloc(4); b.writeUInt32LE(n); return b }

test('limit_order_create byte layout', () => {
const { serializedTx, tier } = sign(['limit_order_create', CREATE])
expect(tier).toBe('active')
expect(serializedTx.equals(txBytes(1,
Buffer.from([5]), vstr('alice'), u32le(42),
vasset(1500n, 3, 'HIVE'), vasset(400n, 3, 'HBD'),
Buffer.from([1]), u32le(1700003600),
))).toBe(true)
})

test('limit_order_cancel byte layout', () => {
const { serializedTx, tier } = sign(['limit_order_cancel', { owner: 'alice', orderid: 42 }])
expect(tier).toBe('active')
expect(serializedTx.equals(txBytes(1,
Buffer.from([6]), vstr('alice'), u32le(42),
))).toBe(true)
})

test('fill_or_kill false serializes as 0', () => {
const { serializedTx } = sign(['limit_order_create', { ...CREATE, fill_or_kill: false }])
expect(serializedTx[serializedTx.length - 6]).toBe(0)
})

// Degenerate orders the firmware rejects — mirrored host-side for a clearer error
test('same symbol on both sides is rejected', () => {
expect(() => sign(['limit_order_create', { ...CREATE, min_to_receive: '0.400 HIVE' }]))
.toThrow(/symbols must differ/)
})
test('zero amount_to_sell is rejected', () => {
expect(() => sign(['limit_order_create', { ...CREATE, amount_to_sell: '0.000 HIVE' }]))
.toThrow(/greater than zero/)
})
test('zero min_to_receive is rejected', () => {
expect(() => sign(['limit_order_create', { ...CREATE, min_to_receive: '0.000 HBD' }]))
.toThrow(/greater than zero/)
})
test('VESTS never trades on the internal market', () => {
expect(() => sign(['limit_order_create', { ...CREATE, amount_to_sell: '1.500000 VESTS' }]))
.toThrow(/must be HIVE or HBD/)
})
test('non-boolean fill_or_kill is rejected', () => {
expect(() => sign(['limit_order_create', { ...CREATE, fill_or_kill: 1 }]))
.toThrow(/must be a boolean/)
})
test('cannot share a tx with posting-tier ops', () => {
expect(() => sign(
['limit_order_cancel', { owner: 'alice', orderid: 1 }],
['vote', { voter: 'alice', author: 'bob', permlink: 'p', weight: 10000 }],
)).toThrow(/Cannot mix posting-tier and active-tier/)
})
})
35 changes: 35 additions & 0 deletions projects/keepkey-vault/src/bun/txbuilder/hive-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* (3), withdraw_vesting (4), convert (8), comment_options (19),
* transfer_to/from_savings (32/33), claim_reward_balance (39),
* delegate_vesting_shares (40), account_update2 (43, metadata-only).
* Phase-3 ops: limit_order_create (5), limit_order_cancel (6) — the internal
* market, required for in-wallet HIVE/HBD swaps.
*
* Byte-exact mirror of hived's condenser serialization and the independent
* python-keepkey test serializer (test_msg_hive.py _op_* helpers). The
Expand All @@ -21,6 +23,8 @@ const OP_VOTE = 0
const OP_COMMENT = 1
const OP_TRANSFER_TO_VESTING = 3
const OP_WITHDRAW_VESTING = 4
const OP_LIMIT_ORDER_CREATE = 5
const OP_LIMIT_ORDER_CANCEL = 6
const OP_CONVERT = 8
const OP_CUSTOM_JSON = 18
const OP_COMMENT_OPTIONS = 19
Expand Down Expand Up @@ -174,6 +178,37 @@ function serializeOp([name, p]: HiveOpTuple): { bytes: Buffer; tier: 'posting' |
]),
tier: 'active',
}
case 'limit_order_create': {
const sell = positiveAsset(p.amount_to_sell, ['HIVE', 'HBD'], 'limit_order_create amount_to_sell')
const recv = positiveAsset(p.min_to_receive, ['HIVE', 'HBD'], 'limit_order_create min_to_receive')
// A same-symbol order is a no-op trade on screen but still burns the
// fill; firmware refuses it, reject here for a clearer message.
if (sell.subarray(9, 16).equals(recv.subarray(9, 16))) {
throw new Error('limit_order_create: amount_to_sell and min_to_receive symbols must differ')
}
// expiration is unbounded here: the device has no RTC and the only cost
// of a stale/far-future value is an on-chain rejection, not a bad sign.
return {
bytes: Buffer.concat([
varint(OP_LIMIT_ORDER_CREATE),
str(p.owner),
u32(Number(p.orderid), 'limit_order_create orderid'),
sell, recv,
boolByte(p.fill_or_kill, 'limit_order_create fill_or_kill'),
u32(Number(p.expiration), 'limit_order_create expiration'),
]),
tier: 'active',
}
}
case 'limit_order_cancel':
return {
bytes: Buffer.concat([
varint(OP_LIMIT_ORDER_CANCEL),
str(p.owner),
u32(Number(p.orderid), 'limit_order_cancel orderid'),
]),
tier: 'active',
}
case 'convert':
return {
bytes: Buffer.concat([
Expand Down