@qubic.org/contracts 0.2.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +41 -0
- package/README.md +15 -18
- package/dist/__generated__/computorcontrolledfund.d.ts +1 -1
- package/dist/__generated__/computorcontrolledfund.d.ts.map +1 -1
- package/dist/__generated__/computorcontrolledfund.js +8 -8
- package/dist/__generated__/computorcontrolledfund.js.map +1 -1
- package/dist/__generated__/escrow.d.ts +1 -1
- package/dist/__generated__/escrow.d.ts.map +1 -1
- package/dist/__generated__/escrow.js +3 -3
- package/dist/__generated__/escrow.js.map +1 -1
- package/dist/__generated__/generalquorumproposal.d.ts +1 -1
- package/dist/__generated__/generalquorumproposal.d.ts.map +1 -1
- package/dist/__generated__/generalquorumproposal.js +6 -6
- package/dist/__generated__/generalquorumproposal.js.map +1 -1
- package/dist/__generated__/ggwp.d.ts +265 -0
- package/dist/__generated__/ggwp.d.ts.map +1 -0
- package/dist/__generated__/ggwp.js +825 -0
- package/dist/__generated__/ggwp.js.map +1 -0
- package/dist/__generated__/index.d.ts +1 -0
- package/dist/__generated__/index.d.ts.map +1 -1
- package/dist/__generated__/index.js +1 -0
- package/dist/__generated__/index.js.map +1 -1
- package/dist/__generated__/msvault.d.ts +1 -1
- package/dist/__generated__/msvault.d.ts.map +1 -1
- package/dist/__generated__/msvault.js +19 -19
- package/dist/__generated__/msvault.js.map +1 -1
- package/dist/__generated__/multisignvault.d.ts +25 -25
- package/dist/__generated__/multisignvault.d.ts.map +1 -1
- package/dist/__generated__/multisignvault.js +25 -25
- package/dist/__generated__/multisignvault.js.map +1 -1
- package/dist/__generated__/nostromo.d.ts +1 -1
- package/dist/__generated__/nostromo.d.ts.map +1 -1
- package/dist/__generated__/nostromo.js +15 -15
- package/dist/__generated__/nostromo.js.map +1 -1
- package/dist/__generated__/pulse.d.ts +1 -1
- package/dist/__generated__/pulse.d.ts.map +1 -1
- package/dist/__generated__/pulse.js +26 -26
- package/dist/__generated__/pulse.js.map +1 -1
- package/dist/__generated__/qbay.d.ts +1 -1
- package/dist/__generated__/qbay.d.ts.map +1 -1
- package/dist/__generated__/qbay.js +23 -23
- package/dist/__generated__/qbay.js.map +1 -1
- package/dist/__generated__/qbond.d.ts +1 -1
- package/dist/__generated__/qbond.d.ts.map +1 -1
- package/dist/__generated__/qbond.js +14 -14
- package/dist/__generated__/qbond.js.map +1 -1
- package/dist/__generated__/qdraw.d.ts +1 -1
- package/dist/__generated__/qdraw.d.ts.map +1 -1
- package/dist/__generated__/qdraw.js.map +1 -1
- package/dist/__generated__/qduel.d.ts +1 -1
- package/dist/__generated__/qduel.d.ts.map +1 -1
- package/dist/__generated__/qduel.js +11 -11
- package/dist/__generated__/qduel.js.map +1 -1
- package/dist/__generated__/qearn.d.ts +1 -1
- package/dist/__generated__/qearn.d.ts.map +1 -1
- package/dist/__generated__/qearn.js +10 -10
- package/dist/__generated__/qearn.js.map +1 -1
- package/dist/__generated__/qip.d.ts +18 -1
- package/dist/__generated__/qip.d.ts.map +1 -1
- package/dist/__generated__/qip.js +64 -3
- package/dist/__generated__/qip.js.map +1 -1
- package/dist/__generated__/qraffle.d.ts +1 -1
- package/dist/__generated__/qraffle.d.ts.map +1 -1
- package/dist/__generated__/qraffle.js +12 -12
- package/dist/__generated__/qraffle.js.map +1 -1
- package/dist/__generated__/qreservepool.d.ts +1 -1
- package/dist/__generated__/qreservepool.d.ts.map +1 -1
- package/dist/__generated__/qreservepool.js +5 -5
- package/dist/__generated__/qreservepool.js.map +1 -1
- package/dist/__generated__/qrwa.d.ts +1 -1
- package/dist/__generated__/qrwa.d.ts.map +1 -1
- package/dist/__generated__/qrwa.js +18 -18
- package/dist/__generated__/qrwa.js.map +1 -1
- package/dist/__generated__/qswap.d.ts +3 -1
- package/dist/__generated__/qswap.d.ts.map +1 -1
- package/dist/__generated__/qswap.js +30 -18
- package/dist/__generated__/qswap.js.map +1 -1
- package/dist/__generated__/qthirtyfour.d.ts +1 -1
- package/dist/__generated__/qthirtyfour.d.ts.map +1 -1
- package/dist/__generated__/qthirtyfour.js +19 -19
- package/dist/__generated__/qthirtyfour.js.map +1 -1
- package/dist/__generated__/qubicicoportal.d.ts +4 -4
- package/dist/__generated__/qubicicoportal.d.ts.map +1 -1
- package/dist/__generated__/qubicicoportal.js +4 -4
- package/dist/__generated__/qubicicoportal.js.map +1 -1
- package/dist/__generated__/quottery.d.ts +1 -1
- package/dist/__generated__/quottery.d.ts.map +1 -1
- package/dist/__generated__/quottery.js +14 -14
- package/dist/__generated__/quottery.js.map +1 -1
- package/dist/__generated__/qusino.d.ts +1 -1
- package/dist/__generated__/qusino.d.ts.map +1 -1
- package/dist/__generated__/qusino.js +13 -13
- package/dist/__generated__/qusino.js.map +1 -1
- package/dist/__generated__/qutil.d.ts +1 -1
- package/dist/__generated__/qutil.d.ts.map +1 -1
- package/dist/__generated__/qutil.js +22 -22
- package/dist/__generated__/qutil.js.map +1 -1
- package/dist/__generated__/qvault.d.ts +14 -2
- package/dist/__generated__/qvault.d.ts.map +1 -1
- package/dist/__generated__/qvault.js +70 -32
- package/dist/__generated__/qvault.js.map +1 -1
- package/dist/__generated__/qx.d.ts +1 -1
- package/dist/__generated__/qx.d.ts.map +1 -1
- package/dist/__generated__/qx.js +12 -12
- package/dist/__generated__/qx.js.map +1 -1
- package/dist/__generated__/random.d.ts +24 -1
- package/dist/__generated__/random.d.ts.map +1 -1
- package/dist/__generated__/random.js +68 -0
- package/dist/__generated__/random.js.map +1 -1
- package/dist/__generated__/randomlottery.d.ts +1 -1
- package/dist/__generated__/randomlottery.d.ts.map +1 -1
- package/dist/__generated__/randomlottery.js +12 -12
- package/dist/__generated__/randomlottery.js.map +1 -1
- package/dist/__generated__/vottunbridge.d.ts +1 -1
- package/dist/__generated__/vottunbridge.d.ts.map +1 -1
- package/dist/__generated__/vottunbridge.js +13 -13
- package/dist/__generated__/vottunbridge.js.map +1 -1
- package/dist/call-contract.d.ts.map +1 -1
- package/dist/call-contract.js +6 -1
- package/dist/call-contract.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/__generated__/computorcontrolledfund.ts +1066 -0
- package/src/__generated__/escrow.ts +529 -0
- package/src/__generated__/generalquorumproposal.ts +691 -0
- package/src/__generated__/ggwp.ts +1241 -0
- package/src/__generated__/index.ts +28 -0
- package/src/__generated__/msvault.ts +1716 -0
- package/src/__generated__/multisignvault.ts +1511 -0
- package/src/__generated__/mylastmatch.ts +12 -0
- package/src/__generated__/nostromo.ts +1551 -0
- package/src/__generated__/pulse.ts +1512 -0
- package/src/__generated__/qbay.ts +2097 -0
- package/src/__generated__/qbond.ts +1016 -0
- package/src/__generated__/qdraw.ts +188 -0
- package/src/__generated__/qduel.ts +827 -0
- package/src/__generated__/qearn.ts +696 -0
- package/src/__generated__/qip.ts +501 -0
- package/src/__generated__/qraffle.ts +1162 -0
- package/src/__generated__/qreservepool.ts +281 -0
- package/src/__generated__/qrwa.ts +1491 -0
- package/src/__generated__/qswap.ts +1336 -0
- package/src/__generated__/qthirtyfour.ts +1131 -0
- package/src/__generated__/qubicicoportal.ts +386 -0
- package/src/__generated__/quottery.ts +1519 -0
- package/src/__generated__/qusino.ts +799 -0
- package/src/__generated__/qutil.ts +1506 -0
- package/src/__generated__/qvault.ts +2911 -0
- package/src/__generated__/qx.ts +861 -0
- package/src/__generated__/random.ts +145 -0
- package/src/__generated__/randomlottery.ts +653 -0
- package/src/__generated__/supplywatcher.ts +12 -0
- package/src/__generated__/vottunbridge.ts +1162 -0
- package/src/__tests__/contracts.test.ts +933 -0
- package/src/call-contract.ts +87 -0
- package/src/index.ts +9 -0
- package/dist/result.d.ts +0 -12
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -7
- package/dist/result.js.map +0 -1
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { QubicRpcError, err, ok } from '@qubic.org/rpc'
|
|
3
|
+
import {
|
|
4
|
+
// qearn
|
|
5
|
+
QEARN_CONTRACT_INDEX,
|
|
6
|
+
QEARN_GET_STATE_OF_ROUND_INPUT_TYPE,
|
|
7
|
+
QEARN_UNLOCK_INPUT_TYPE,
|
|
8
|
+
buildQearnUnlockInput,
|
|
9
|
+
decodeQearnGetStateOfRoundOutput,
|
|
10
|
+
decodeQearnLockOutput,
|
|
11
|
+
decodeQearnUnlockOutput,
|
|
12
|
+
qearn,
|
|
13
|
+
qearnGetStateOfRound,
|
|
14
|
+
} from '../__generated__/qearn.js'
|
|
15
|
+
import { callContractFunction } from '../call-contract.js'
|
|
16
|
+
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
// Contract metadata for parameterized tests
|
|
19
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Maps each generated contract to its expected CONTRACT_INDEX, module name, and namespace. */
|
|
22
|
+
const CONTRACT_META: {
|
|
23
|
+
name: string
|
|
24
|
+
module: string
|
|
25
|
+
index: number
|
|
26
|
+
namespace: string | null
|
|
27
|
+
}[] = [
|
|
28
|
+
{ name: 'qx', module: 'qx.js', index: 1, namespace: 'qx' },
|
|
29
|
+
{ name: 'quottery', module: 'quottery.js', index: 2, namespace: 'quottery' },
|
|
30
|
+
{ name: 'random', module: 'random.js', index: 3, namespace: 'random' },
|
|
31
|
+
{ name: 'qutil', module: 'qutil.js', index: 4, namespace: 'qUtil' },
|
|
32
|
+
{ name: 'mylastmatch', module: 'mylastmatch.js', index: 5, namespace: 'myLastMatch' },
|
|
33
|
+
{
|
|
34
|
+
name: 'generalquorumproposal',
|
|
35
|
+
module: 'generalquorumproposal.js',
|
|
36
|
+
index: 6,
|
|
37
|
+
namespace: 'generalQuorumProposal',
|
|
38
|
+
},
|
|
39
|
+
{ name: 'supplywatcher', module: 'supplywatcher.js', index: 7, namespace: 'supplyWatcher' },
|
|
40
|
+
{
|
|
41
|
+
name: 'computorcontrolledfund',
|
|
42
|
+
module: 'computorcontrolledfund.js',
|
|
43
|
+
index: 8,
|
|
44
|
+
namespace: 'computorControlledFund',
|
|
45
|
+
},
|
|
46
|
+
{ name: 'qearn', module: 'qearn.js', index: 9, namespace: 'qearn' },
|
|
47
|
+
{ name: 'qvault', module: 'qvault.js', index: 10, namespace: 'qVAULT' },
|
|
48
|
+
{ name: 'msvault', module: 'msvault.js', index: 11, namespace: 'msVault' },
|
|
49
|
+
{ name: 'multisignvault', module: 'multisignvault.js', index: 11, namespace: null },
|
|
50
|
+
{ name: 'qbay', module: 'qbay.js', index: 12, namespace: 'qbay' },
|
|
51
|
+
{ name: 'qswap', module: 'qswap.js', index: 13, namespace: 'qswap' },
|
|
52
|
+
{ name: 'nostromo', module: 'nostromo.js', index: 14, namespace: 'nostromo' },
|
|
53
|
+
{ name: 'qdraw', module: 'qdraw.js', index: 15, namespace: 'qdraw' },
|
|
54
|
+
{ name: 'randomlottery', module: 'randomlottery.js', index: 16, namespace: 'randomLottery' },
|
|
55
|
+
{ name: 'qbond', module: 'qbond.js', index: 17, namespace: 'qBond' },
|
|
56
|
+
{ name: 'qip', module: 'qip.js', index: 18, namespace: 'qIP' },
|
|
57
|
+
{ name: 'qraffle', module: 'qraffle.js', index: 19, namespace: 'qRaffle' },
|
|
58
|
+
{ name: 'qrwa', module: 'qrwa.js', index: 20, namespace: 'qRWA' },
|
|
59
|
+
{ name: 'qreservepool', module: 'qreservepool.js', index: 21, namespace: 'qReservePool' },
|
|
60
|
+
{ name: 'qthirtyfour', module: 'qthirtyfour.js', index: 22, namespace: 'qThirtyFour' },
|
|
61
|
+
{ name: 'qduel', module: 'qduel.js', index: 23, namespace: 'qDuel' },
|
|
62
|
+
{ name: 'pulse', module: 'pulse.js', index: 24, namespace: 'pulse' },
|
|
63
|
+
{ name: 'vottunbridge', module: 'vottunbridge.js', index: 25, namespace: 'vottunBridge' },
|
|
64
|
+
{ name: 'qusino', module: 'qusino.js', index: 26, namespace: 'qusino' },
|
|
65
|
+
{ name: 'escrow', module: 'escrow.js', index: 27, namespace: 'escrow' },
|
|
66
|
+
{ name: 'ggwp', module: 'ggwp.js', index: 28, namespace: 'gGWP' },
|
|
67
|
+
{ name: 'qubicicoportal', module: 'qubicicoportal.js', index: 18, namespace: null },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
// ──────────────────────────────────────────────
|
|
71
|
+
// Dynamic imports for parameterized tests
|
|
72
|
+
// ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Lazily loaded contract modules keyed by module filename. */
|
|
75
|
+
const moduleCache = new Map<string, Record<string, unknown>>()
|
|
76
|
+
|
|
77
|
+
async function getContractModule(moduleFile: string): Promise<Record<string, unknown>> {
|
|
78
|
+
const cached = moduleCache.get(moduleFile)
|
|
79
|
+
if (cached) return cached
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
81
|
+
const mod = await import(`../__generated__/${moduleFile}`)
|
|
82
|
+
moduleCache.set(moduleFile, mod as Record<string, unknown>)
|
|
83
|
+
return mod as Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
// 1. Existing qearn tests (preserved from original)
|
|
88
|
+
// ──────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe('generated constants', () => {
|
|
91
|
+
test('QEARN_CONTRACT_INDEX is 9', () => {
|
|
92
|
+
expect(QEARN_CONTRACT_INDEX).toBe(9)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('QEARN_UNLOCK_INPUT_TYPE is 2', () => {
|
|
96
|
+
// REGISTER_USER_PROCEDURE(unlock, 2) in Qearn.h
|
|
97
|
+
expect(QEARN_UNLOCK_INPUT_TYPE).toBe(2)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('QEARN_GET_STATE_OF_ROUND_INPUT_TYPE is 3', () => {
|
|
101
|
+
expect(QEARN_GET_STATE_OF_ROUND_INPUT_TYPE).toBe(3)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('buildQearnUnlockInput', () => {
|
|
106
|
+
test('returns ContractCall with correct contractIndex and inputType', () => {
|
|
107
|
+
const call = buildQearnUnlockInput({ amount: 5_000_000n, lockedEpoch: 150 })
|
|
108
|
+
expect(call.contractIndex).toBe(QEARN_CONTRACT_INDEX)
|
|
109
|
+
expect(call.inputType).toBe(QEARN_UNLOCK_INPUT_TYPE)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('payload is 12 bytes', () => {
|
|
113
|
+
const { payload } = buildQearnUnlockInput({ amount: 5_000_000n, lockedEpoch: 150 })
|
|
114
|
+
expect(payload.byteLength).toBe(12)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('encodes amount as uint64 LE', () => {
|
|
118
|
+
const { payload } = buildQearnUnlockInput({ amount: 5_000_000n, lockedEpoch: 150 })
|
|
119
|
+
expect(new DataView(payload.buffer).getBigUint64(0, true)).toBe(5_000_000n)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('encodes lockedEpoch as uint32 LE', () => {
|
|
123
|
+
const { payload } = buildQearnUnlockInput({ amount: 5_000_000n, lockedEpoch: 150 })
|
|
124
|
+
expect(new DataView(payload.buffer).getUint32(8, true)).toBe(150)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('decodeQearnUnlockOutput', () => {
|
|
129
|
+
test('decodes returnCode', () => {
|
|
130
|
+
const data = new Uint8Array(4)
|
|
131
|
+
new DataView(data.buffer).setUint32(0, 42, true)
|
|
132
|
+
const output = decodeQearnUnlockOutput(data)
|
|
133
|
+
expect(output.returnCode).toBe(42)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('decodeQearnLockOutput', () => {
|
|
138
|
+
test('decodes returnCode', () => {
|
|
139
|
+
const data = new Uint8Array(4)
|
|
140
|
+
new DataView(data.buffer).setUint32(0, 0, true)
|
|
141
|
+
expect(decodeQearnLockOutput(data).returnCode).toBe(0)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('buildQearnGetStateOfRoundInput', () => {
|
|
146
|
+
test('encodes epoch as uint32 LE via namespace buildUnlockInput', () => {
|
|
147
|
+
const call = qearn.buildUnlockInput({ amount: 1000n, lockedEpoch: 212 })
|
|
148
|
+
expect(call.contractIndex).toBe(QEARN_CONTRACT_INDEX)
|
|
149
|
+
expect(call.inputType).toBe(QEARN_UNLOCK_INPUT_TYPE)
|
|
150
|
+
expect(call.payload.byteLength).toBe(12)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('decodeQearnGetStateOfRoundOutput', () => {
|
|
155
|
+
test('decodes state', () => {
|
|
156
|
+
const data = new Uint8Array(4)
|
|
157
|
+
new DataView(data.buffer).setUint32(0, 99, true)
|
|
158
|
+
expect(decodeQearnGetStateOfRoundOutput(data).state).toBe(99)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('high-level function wrappers', () => {
|
|
163
|
+
function makeBase64(bytes: Uint8Array): string {
|
|
164
|
+
let s = ''
|
|
165
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i] ?? 0)
|
|
166
|
+
return btoa(s)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
test('qearnGetStateOfRound returns decoded output', async () => {
|
|
170
|
+
const responseData = new Uint8Array(4)
|
|
171
|
+
new DataView(responseData.buffer).setUint32(0, 99, true)
|
|
172
|
+
const live = {
|
|
173
|
+
async querySmartContract() {
|
|
174
|
+
return ok({ responseData: makeBase64(responseData) })
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
const result = await qearnGetStateOfRound(live, { epoch: 212 })
|
|
178
|
+
expect(result.ok).toBe(true)
|
|
179
|
+
if (result.ok) expect(result.value.state).toBe(99)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('qearn.getStateOfRound is the same function via namespace', async () => {
|
|
183
|
+
expect(qearn.getStateOfRound).toBe(qearnGetStateOfRound)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('qearn.contractIndex is 9', () => {
|
|
187
|
+
expect(qearn.contractIndex).toBe(9)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('qearn.buildUnlockInput returns ContractCall', () => {
|
|
191
|
+
const call = qearn.buildUnlockInput({ amount: 1000n, lockedEpoch: 212 })
|
|
192
|
+
expect(call.contractIndex).toBe(QEARN_CONTRACT_INDEX)
|
|
193
|
+
expect(call.inputType).toBe(QEARN_UNLOCK_INPUT_TYPE)
|
|
194
|
+
expect(call.payload.byteLength).toBe(12)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// ──────────────────────────────────────────────
|
|
199
|
+
// 2. Parameterized tests for ALL generated contracts
|
|
200
|
+
// ──────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
describe('parameterized: every contract module exports CONTRACT_INDEX', () => {
|
|
203
|
+
for (const meta of CONTRACT_META) {
|
|
204
|
+
test(`${meta.name} exports ${meta.module.replace('.js', '')}_CONTRACT_INDEX = ${meta.index}`, async () => {
|
|
205
|
+
const mod = await getContractModule(meta.module)
|
|
206
|
+
const indexKey = Object.keys(mod).find((k) => k.endsWith('_CONTRACT_INDEX'))
|
|
207
|
+
expect(indexKey).toBeDefined()
|
|
208
|
+
expect(mod[indexKey!]).toBe(meta.index)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('parameterized: every contract namespace has correct contractIndex', () => {
|
|
214
|
+
for (const meta of CONTRACT_META) {
|
|
215
|
+
if (!meta.namespace) continue
|
|
216
|
+
test(`${meta.namespace}.contractIndex === ${meta.index}`, async () => {
|
|
217
|
+
const mod = await getContractModule(meta.module)
|
|
218
|
+
const ns = mod[meta.namespace!] as Record<string, unknown> | undefined
|
|
219
|
+
expect(ns).toBeDefined()
|
|
220
|
+
expect(ns!.contractIndex).toBe(meta.index)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('parameterized: every contract has at least one builder function', () => {
|
|
226
|
+
for (const meta of CONTRACT_META) {
|
|
227
|
+
if (meta.name === 'mylastmatch' || meta.name === 'supplywatcher') continue // empty contracts
|
|
228
|
+
test(`${meta.name} exports a buildXxxInput function`, async () => {
|
|
229
|
+
const mod = await getContractModule(meta.module)
|
|
230
|
+
const buildKeys = Object.keys(mod).filter((k) => k.startsWith('build') && k.endsWith('Input'))
|
|
231
|
+
expect(buildKeys.length).toBeGreaterThan(0)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('parameterized: every contract has at least one decoder function', () => {
|
|
237
|
+
for (const meta of CONTRACT_META) {
|
|
238
|
+
if (meta.name === 'mylastmatch' || meta.name === 'supplywatcher') continue // empty contracts
|
|
239
|
+
test(`${meta.name} exports a decodeXxxOutput function`, async () => {
|
|
240
|
+
const mod = await getContractModule(meta.module)
|
|
241
|
+
const decodeKeys = Object.keys(mod).filter(
|
|
242
|
+
(k) => k.startsWith('decode') && k.endsWith('Output'),
|
|
243
|
+
)
|
|
244
|
+
expect(decodeKeys.length).toBeGreaterThan(0)
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('parameterized: every contract INPUT_TYPE is a positive number', () => {
|
|
250
|
+
for (const meta of CONTRACT_META) {
|
|
251
|
+
if (meta.name === 'mylastmatch' || meta.name === 'supplywatcher') continue // empty contracts
|
|
252
|
+
test(`${meta.name} INPUT_TYPE constants are positive numbers`, async () => {
|
|
253
|
+
const mod = await getContractModule(meta.module)
|
|
254
|
+
const inputTypeKeys = Object.keys(mod).filter((k) => k.endsWith('_INPUT_TYPE'))
|
|
255
|
+
expect(inputTypeKeys.length).toBeGreaterThan(0)
|
|
256
|
+
for (const key of inputTypeKeys) {
|
|
257
|
+
const val = mod[key]
|
|
258
|
+
expect(typeof val).toBe('number')
|
|
259
|
+
expect(val as number).toBeGreaterThan(0)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('parameterized: builder and decoder are functions', () => {
|
|
266
|
+
for (const meta of CONTRACT_META) {
|
|
267
|
+
if (meta.name === 'mylastmatch' || meta.name === 'supplywatcher') continue // empty contracts
|
|
268
|
+
test(`${meta.name} builder and decoder are callable functions`, async () => {
|
|
269
|
+
const mod = await getContractModule(meta.module)
|
|
270
|
+
|
|
271
|
+
const buildKeys = Object.keys(mod).filter((k) => k.startsWith('build') && k.endsWith('Input'))
|
|
272
|
+
const decodeKeys = Object.keys(mod).filter(
|
|
273
|
+
(k) => k.startsWith('decode') && k.endsWith('Output'),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
// At least one builder and one decoder should exist
|
|
277
|
+
expect(buildKeys.length).toBeGreaterThan(0)
|
|
278
|
+
expect(decodeKeys.length).toBeGreaterThan(0)
|
|
279
|
+
|
|
280
|
+
// All builders and decoders should be functions
|
|
281
|
+
for (const key of buildKeys) {
|
|
282
|
+
expect(typeof mod[key]).toBe('function')
|
|
283
|
+
}
|
|
284
|
+
for (const key of decodeKeys) {
|
|
285
|
+
expect(typeof mod[key]).toBe('function')
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ──────────────────────────────────────────────
|
|
292
|
+
// 3. Struct-using contract tests (qx with Asset struct)
|
|
293
|
+
// ──────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe('qx struct payload encoding', () => {
|
|
296
|
+
test('buildQxIssueAssetInput returns correct ContractCall', async () => {
|
|
297
|
+
const { buildQxIssueAssetInput, QX_CONTRACT_INDEX, QX_ISSUE_ASSET_INPUT_TYPE } =
|
|
298
|
+
(await getContractModule('qx.js')) as typeof import('../__generated__/qx.js')
|
|
299
|
+
const call = buildQxIssueAssetInput({
|
|
300
|
+
assetName: 12345n,
|
|
301
|
+
numberOfShares: 1_000_000n,
|
|
302
|
+
unitOfMeasurement: 1n,
|
|
303
|
+
numberOfDecimalPlaces: 4,
|
|
304
|
+
})
|
|
305
|
+
expect(call.contractIndex).toBe(QX_CONTRACT_INDEX)
|
|
306
|
+
expect(call.inputType).toBe(QX_ISSUE_ASSET_INPUT_TYPE)
|
|
307
|
+
// uint64(8) + sint64(8) + uint64(8) + sint8(1) = 25 bytes
|
|
308
|
+
expect(call.payload.byteLength).toBe(25)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('buildQxIssueAssetInput encodes fields correctly', async () => {
|
|
312
|
+
const { buildQxIssueAssetInput } = (await getContractModule('qx.js')) as typeof import('../__generated__/qx.js')
|
|
313
|
+
const call = buildQxIssueAssetInput({
|
|
314
|
+
assetName: 0xabcdn,
|
|
315
|
+
numberOfShares: 999n,
|
|
316
|
+
unitOfMeasurement: 10n,
|
|
317
|
+
numberOfDecimalPlaces: 2,
|
|
318
|
+
})
|
|
319
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
320
|
+
// assetName at offset 0, uint64 LE
|
|
321
|
+
expect(view.getBigUint64(0, true)).toBe(0xabcdn)
|
|
322
|
+
// numberOfShares at offset 8, sint64 LE
|
|
323
|
+
expect(view.getBigInt64(8, true)).toBe(999n)
|
|
324
|
+
// unitOfMeasurement at offset 16, uint64 LE
|
|
325
|
+
expect(view.getBigUint64(16, true)).toBe(10n)
|
|
326
|
+
// numberOfDecimalPlaces at offset 24, sint8
|
|
327
|
+
expect(new Int8Array(call.payload.buffer, call.payload.byteOffset + 24, 1)[0]).toBe(2)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('decodeQxIssueAssetOutput decodes issuedNumberOfShares', async () => {
|
|
331
|
+
const { decodeQxIssueAssetOutput } = (await getContractModule('qx.js')) as typeof import('../__generated__/qx.js')
|
|
332
|
+
const data = new Uint8Array(8)
|
|
333
|
+
new DataView(data.buffer).setBigInt64(0, 5000n, true)
|
|
334
|
+
const output = decodeQxIssueAssetOutput(data)
|
|
335
|
+
expect(output.issuedNumberOfShares).toBe(5000n)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('buildQxTransferShareManagementRightsInput encodes struct field', async () => {
|
|
339
|
+
const { buildQxTransferShareManagementRightsInput } = (await getContractModule(
|
|
340
|
+
'qx.js',
|
|
341
|
+
)) as typeof import('../__generated__/qx.js')
|
|
342
|
+
const call = buildQxTransferShareManagementRightsInput({
|
|
343
|
+
asset: { issuer: 'A'.repeat(55), assetName: 42n },
|
|
344
|
+
numberOfShares: 100n,
|
|
345
|
+
newManagingContractIndex: 13,
|
|
346
|
+
})
|
|
347
|
+
// struct(40) + sint64(8) + uint32(4) = 52 bytes
|
|
348
|
+
expect(call.payload.byteLength).toBe(52)
|
|
349
|
+
expect(call.contractIndex).toBe(1)
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// ──────────────────────────────────────────────
|
|
354
|
+
// 4. escrow struct encoding tests (Deal struct with arrays of structs)
|
|
355
|
+
// ──────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
describe('escrow struct payload encoding', () => {
|
|
358
|
+
test('buildEscrowAcceptDealInput encodes sint64 index', async () => {
|
|
359
|
+
const { buildEscrowAcceptDealInput, ESCROW_CONTRACT_INDEX, ESCROW_ACCEPT_DEAL_INPUT_TYPE } =
|
|
360
|
+
(await getContractModule('escrow.js')) as typeof import('../__generated__/escrow.js')
|
|
361
|
+
const call = buildEscrowAcceptDealInput({ index: 42n })
|
|
362
|
+
expect(call.contractIndex).toBe(ESCROW_CONTRACT_INDEX)
|
|
363
|
+
expect(call.inputType).toBe(ESCROW_ACCEPT_DEAL_INPUT_TYPE)
|
|
364
|
+
expect(call.payload.byteLength).toBe(8)
|
|
365
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
366
|
+
expect(view.getBigInt64(0, true)).toBe(42n)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('decodeEscrowTransferShareManagementRightsOutput decodes transferredShares', async () => {
|
|
370
|
+
const { decodeEscrowTransferShareManagementRightsOutput } = (await getContractModule(
|
|
371
|
+
'escrow.js',
|
|
372
|
+
)) as typeof import('../__generated__/escrow.js')
|
|
373
|
+
const data = new Uint8Array(8)
|
|
374
|
+
new DataView(data.buffer).setBigInt64(0, 555n, true)
|
|
375
|
+
const output = decodeEscrowTransferShareManagementRightsOutput(data)
|
|
376
|
+
expect(output.transferredShares).toBe(555n)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// ──────────────────────────────────────────────
|
|
381
|
+
// 5. qswap struct encoding tests (Asset struct)
|
|
382
|
+
// ──────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
describe('qswap struct payload encoding', () => {
|
|
385
|
+
test('buildQswapCreatePoolInput encodes id + uint64', async () => {
|
|
386
|
+
const { buildQswapCreatePoolInput, QSWAP_CONTRACT_INDEX, QSWAP_CREATE_POOL_INPUT_TYPE } =
|
|
387
|
+
(await getContractModule('qswap.js')) as typeof import('../__generated__/qswap.js')
|
|
388
|
+
const idToPk = (_id: string) => new Uint8Array(32)
|
|
389
|
+
const call = buildQswapCreatePoolInput({ assetIssuer: 'X'.repeat(55), assetName: 7n }, idToPk)
|
|
390
|
+
expect(call.contractIndex).toBe(QSWAP_CONTRACT_INDEX)
|
|
391
|
+
expect(call.inputType).toBe(QSWAP_CREATE_POOL_INPUT_TYPE)
|
|
392
|
+
// id(32) + uint64(8) = 40 bytes
|
|
393
|
+
expect(call.payload.byteLength).toBe(40)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('buildQswapFeesInput (no input) uses empty payload', async () => {
|
|
397
|
+
const { qswapFees, QSWAP_CONTRACT_INDEX } = (await getContractModule(
|
|
398
|
+
'qswap.js',
|
|
399
|
+
)) as typeof import('../__generated__/qswap.js')
|
|
400
|
+
// qswapFees is an async function, but the namespace should have Fees
|
|
401
|
+
// Test the namespace
|
|
402
|
+
const { qswap } = (await getContractModule('qswap.js')) as typeof import('../__generated__/qswap.js')
|
|
403
|
+
expect(qswap.contractIndex).toBe(QSWAP_CONTRACT_INDEX)
|
|
404
|
+
expect(typeof qswapFees).toBe('function')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('decodeQswapFeesOutput decodes multiple uint32 fields', async () => {
|
|
408
|
+
const { decodeQswapFeesOutput } = (await getContractModule('qswap.js')) as typeof import('../__generated__/qswap.js')
|
|
409
|
+
// 8 fields * 4 bytes = 32 bytes
|
|
410
|
+
const data = new Uint8Array(32)
|
|
411
|
+
const view = new DataView(data.buffer)
|
|
412
|
+
view.setUint32(0, 100, true) // assetIssuanceFee
|
|
413
|
+
view.setUint32(4, 200, true) // poolCreationFee
|
|
414
|
+
view.setUint32(8, 300, true) // transferFee
|
|
415
|
+
view.setUint32(12, 400, true) // swapFee
|
|
416
|
+
view.setUint32(16, 500, true) // shareholderFee
|
|
417
|
+
view.setUint32(20, 600, true) // investRewardsFee
|
|
418
|
+
view.setUint32(24, 700, true) // qxFee
|
|
419
|
+
view.setUint32(28, 800, true) // burnFee
|
|
420
|
+
const output = decodeQswapFeesOutput(data)
|
|
421
|
+
expect(output.assetIssuanceFee).toBe(100)
|
|
422
|
+
expect(output.poolCreationFee).toBe(200)
|
|
423
|
+
expect(output.transferFee).toBe(300)
|
|
424
|
+
expect(output.swapFee).toBe(400)
|
|
425
|
+
expect(output.shareholderFee).toBe(500)
|
|
426
|
+
expect(output.investRewardsFee).toBe(600)
|
|
427
|
+
expect(output.qxFee).toBe(700)
|
|
428
|
+
expect(output.burnFee).toBe(800)
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// ──────────────────────────────────────────────
|
|
433
|
+
// 6. random contract tests (bytes type fields)
|
|
434
|
+
// ──────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
describe('random contract payload encoding', () => {
|
|
437
|
+
test('buildRandomBuyEntropyInput encodes uint8 + uint16 + id', async () => {
|
|
438
|
+
const { buildRandomBuyEntropyInput, RANDOM_CONTRACT_INDEX, RANDOM_BUY_ENTROPY_INPUT_TYPE } =
|
|
439
|
+
(await getContractModule('random.js')) as typeof import('../__generated__/random.js')
|
|
440
|
+
const idToPk = (_id: string) => {
|
|
441
|
+
const pk = new Uint8Array(32)
|
|
442
|
+
pk[0] = 0xff
|
|
443
|
+
return pk
|
|
444
|
+
}
|
|
445
|
+
const call = buildRandomBuyEntropyInput(
|
|
446
|
+
{ collateralTier: 2, numberOfBits: 256, trustee: 'T'.repeat(55) },
|
|
447
|
+
idToPk,
|
|
448
|
+
)
|
|
449
|
+
expect(call.contractIndex).toBe(RANDOM_CONTRACT_INDEX)
|
|
450
|
+
expect(call.inputType).toBe(RANDOM_BUY_ENTROPY_INPUT_TYPE)
|
|
451
|
+
// uint8(1) + uint16(2) + id(32) = 35 bytes
|
|
452
|
+
expect(call.payload.byteLength).toBe(35)
|
|
453
|
+
expect(call.payload[0]).toBe(2) // collateralTier
|
|
454
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
455
|
+
expect(view.getUint16(1, true)).toBe(256) // numberOfBits
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('decodeRandomBuyEntropyOutput decodes 512-byte entropy', async () => {
|
|
459
|
+
const { decodeRandomBuyEntropyOutput } = (await getContractModule(
|
|
460
|
+
'random.js',
|
|
461
|
+
)) as typeof import('../__generated__/random.js')
|
|
462
|
+
const data = new Uint8Array(512)
|
|
463
|
+
data[0] = 0xaa
|
|
464
|
+
data[511] = 0xbb
|
|
465
|
+
const output = decodeRandomBuyEntropyOutput(data)
|
|
466
|
+
expect(output.entropy.byteLength).toBe(512)
|
|
467
|
+
expect(output.entropy[0]).toBe(0xaa)
|
|
468
|
+
expect(output.entropy[511]).toBe(0xbb)
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// ──────────────────────────────────────────────
|
|
473
|
+
// 7. nostromo contract tests (structs + procedures)
|
|
474
|
+
// ──────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
describe('nostromo contract payload encoding', () => {
|
|
477
|
+
test('buildNostromoRegisterInTierInput encodes uint32', async () => {
|
|
478
|
+
const {
|
|
479
|
+
buildNostromoRegisterInTierInput,
|
|
480
|
+
NOSTROMO_CONTRACT_INDEX,
|
|
481
|
+
NOSTROMO_REGISTER_IN_TIER_INPUT_TYPE,
|
|
482
|
+
} = (await getContractModule('nostromo.js')) as typeof import('../__generated__/nostromo.js')
|
|
483
|
+
const call = buildNostromoRegisterInTierInput({ tierLevel: 3 })
|
|
484
|
+
expect(call.contractIndex).toBe(NOSTROMO_CONTRACT_INDEX)
|
|
485
|
+
expect(call.inputType).toBe(NOSTROMO_REGISTER_IN_TIER_INPUT_TYPE)
|
|
486
|
+
expect(call.payload.byteLength).toBe(4)
|
|
487
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
488
|
+
expect(view.getUint32(0, true)).toBe(3)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
test('decodeNostromoRegisterInTierOutput decodes tierLevel', async () => {
|
|
492
|
+
const { decodeNostromoRegisterInTierOutput } = (await getContractModule(
|
|
493
|
+
'nostromo.js',
|
|
494
|
+
)) as typeof import('../__generated__/nostromo.js')
|
|
495
|
+
const data = new Uint8Array(4)
|
|
496
|
+
new DataView(data.buffer).setUint32(0, 5, true)
|
|
497
|
+
expect(decodeNostromoRegisterInTierOutput(data).tierLevel).toBe(5)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('buildNostromoClaimTokenInput encodes uint64 + uint32', async () => {
|
|
501
|
+
const {
|
|
502
|
+
buildNostromoClaimTokenInput,
|
|
503
|
+
NOSTROMO_CONTRACT_INDEX,
|
|
504
|
+
NOSTROMO_CLAIM_TOKEN_INPUT_TYPE,
|
|
505
|
+
} = (await getContractModule('nostromo.js')) as typeof import('../__generated__/nostromo.js')
|
|
506
|
+
const call = buildNostromoClaimTokenInput({ amount: 1000n, indexOfFundraising: 7 })
|
|
507
|
+
expect(call.contractIndex).toBe(NOSTROMO_CONTRACT_INDEX)
|
|
508
|
+
expect(call.inputType).toBe(NOSTROMO_CLAIM_TOKEN_INPUT_TYPE)
|
|
509
|
+
expect(call.payload.byteLength).toBe(12)
|
|
510
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
511
|
+
expect(view.getBigUint64(0, true)).toBe(1000n)
|
|
512
|
+
expect(view.getUint32(8, true)).toBe(7)
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// ──────────────────────────────────────────────
|
|
517
|
+
// 8. callContractFunction error paths
|
|
518
|
+
// ──────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
describe('callContractFunction', () => {
|
|
521
|
+
function makeBase64(bytes: Uint8Array): string {
|
|
522
|
+
let s = ''
|
|
523
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i] ?? 0)
|
|
524
|
+
return btoa(s)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
test('returns ok result with decoded output', async () => {
|
|
528
|
+
const responseData = new Uint8Array(4)
|
|
529
|
+
new DataView(responseData.buffer).setUint32(0, 7, true)
|
|
530
|
+
|
|
531
|
+
const live = {
|
|
532
|
+
async querySmartContract() {
|
|
533
|
+
return ok({ responseData: makeBase64(responseData) })
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = await callContractFunction<{ epoch: number }, { state: number }>({
|
|
538
|
+
live,
|
|
539
|
+
contractIndex: QEARN_CONTRACT_INDEX,
|
|
540
|
+
inputType: QEARN_GET_STATE_OF_ROUND_INPUT_TYPE,
|
|
541
|
+
inputSize: 4,
|
|
542
|
+
inputFields: [{ name: 'epoch', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
543
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
544
|
+
structs: {},
|
|
545
|
+
input: { epoch: 137 },
|
|
546
|
+
identityToPublicKey: () => new Uint8Array(32),
|
|
547
|
+
publicKeyToIdentity: () => {
|
|
548
|
+
throw new Error('unreachable')
|
|
549
|
+
},
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
expect(result.ok).toBe(true)
|
|
553
|
+
if (result.ok) expect(result.value.state).toBe(7)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test('returns err result when live returns err Result', async () => {
|
|
557
|
+
const live = {
|
|
558
|
+
async querySmartContract() {
|
|
559
|
+
return err(new QubicRpcError(500, '/querySmartContract'))
|
|
560
|
+
},
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const result = await callContractFunction<{ epoch: number }, { state: number }>({
|
|
564
|
+
live,
|
|
565
|
+
contractIndex: QEARN_CONTRACT_INDEX,
|
|
566
|
+
inputType: QEARN_GET_STATE_OF_ROUND_INPUT_TYPE,
|
|
567
|
+
inputSize: 4,
|
|
568
|
+
inputFields: [{ name: 'epoch', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
569
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
570
|
+
structs: {},
|
|
571
|
+
input: { epoch: 137 },
|
|
572
|
+
identityToPublicKey: () => new Uint8Array(32),
|
|
573
|
+
publicKeyToIdentity: () => {
|
|
574
|
+
throw new Error('unreachable')
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
expect(result.ok).toBe(false)
|
|
579
|
+
if (!result.ok) expect(result.error).toBeInstanceOf(QubicRpcError)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('sends correct contractIndex and inputType to live client', async () => {
|
|
583
|
+
let capturedReq: unknown
|
|
584
|
+
const live = {
|
|
585
|
+
async querySmartContract(req: unknown) {
|
|
586
|
+
capturedReq = req
|
|
587
|
+
return ok({ responseData: makeBase64(new Uint8Array(4)) })
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await callContractFunction<{ epoch: number }, { state: number }>({
|
|
592
|
+
live,
|
|
593
|
+
contractIndex: 9,
|
|
594
|
+
inputType: 3,
|
|
595
|
+
inputSize: 4,
|
|
596
|
+
inputFields: [{ name: 'epoch', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
597
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
598
|
+
structs: {},
|
|
599
|
+
input: { epoch: 200 },
|
|
600
|
+
identityToPublicKey: () => new Uint8Array(32),
|
|
601
|
+
publicKeyToIdentity: () => {
|
|
602
|
+
throw new Error('unreachable')
|
|
603
|
+
},
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
const req = capturedReq as { contractIndex?: number; inputType?: number; inputSize?: number }
|
|
607
|
+
expect(req.contractIndex).toBe(9)
|
|
608
|
+
expect(req.inputType).toBe(3)
|
|
609
|
+
expect(req.inputSize).toBe(4)
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
// ── Error path: empty responseData ──
|
|
613
|
+
|
|
614
|
+
test('handles empty responseData gracefully (empty output)', async () => {
|
|
615
|
+
const live = {
|
|
616
|
+
async querySmartContract() {
|
|
617
|
+
return ok({ responseData: '' })
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// With zero output fields, this should succeed
|
|
622
|
+
const result = await callContractFunction<Record<string, never>, Record<string, never>>({
|
|
623
|
+
live,
|
|
624
|
+
contractIndex: 1,
|
|
625
|
+
inputType: 1,
|
|
626
|
+
inputSize: 0,
|
|
627
|
+
inputFields: [],
|
|
628
|
+
outputFields: [],
|
|
629
|
+
structs: {},
|
|
630
|
+
input: {},
|
|
631
|
+
identityToPublicKey: () => new Uint8Array(32),
|
|
632
|
+
publicKeyToIdentity: () => {
|
|
633
|
+
throw new Error('unreachable')
|
|
634
|
+
},
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
expect(result.ok).toBe(true)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test('throws when decoded response has insufficient data for output fields', async () => {
|
|
641
|
+
// responseData is empty but outputFields expects a field -> should throw
|
|
642
|
+
const live = {
|
|
643
|
+
async querySmartContract() {
|
|
644
|
+
return ok({ responseData: '' })
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await expect(
|
|
649
|
+
callContractFunction<{ epoch: number }, { state: number }>({
|
|
650
|
+
live,
|
|
651
|
+
contractIndex: 1,
|
|
652
|
+
inputType: 1,
|
|
653
|
+
inputSize: 4,
|
|
654
|
+
inputFields: [{ name: 'epoch', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
655
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
656
|
+
structs: {},
|
|
657
|
+
input: { epoch: 1 },
|
|
658
|
+
identityToPublicKey: () => new Uint8Array(32),
|
|
659
|
+
publicKeyToIdentity: () => {
|
|
660
|
+
throw new Error('unreachable')
|
|
661
|
+
},
|
|
662
|
+
}),
|
|
663
|
+
).rejects.toThrow()
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
test('propagates QubicRpcError from live client (500)', async () => {
|
|
667
|
+
const live = {
|
|
668
|
+
async querySmartContract() {
|
|
669
|
+
return err(new QubicRpcError(500, '/querySmartContract'))
|
|
670
|
+
},
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const result = await callContractFunction<Record<string, never>, { state: number }>({
|
|
674
|
+
live,
|
|
675
|
+
contractIndex: 9,
|
|
676
|
+
inputType: 3,
|
|
677
|
+
inputSize: 0,
|
|
678
|
+
inputFields: [],
|
|
679
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
680
|
+
structs: {},
|
|
681
|
+
input: {},
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
expect(result.ok).toBe(false)
|
|
685
|
+
if (!result.ok) {
|
|
686
|
+
expect(result.error).toBeInstanceOf(QubicRpcError)
|
|
687
|
+
expect(result.error.status).toBe(500)
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
test('propagates QubicRpcError with 404 status', async () => {
|
|
692
|
+
const live = {
|
|
693
|
+
async querySmartContract() {
|
|
694
|
+
return err(new QubicRpcError(404, '/querySmartContract'))
|
|
695
|
+
},
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const result = await callContractFunction<Record<string, never>, { state: number }>({
|
|
699
|
+
live,
|
|
700
|
+
contractIndex: 9,
|
|
701
|
+
inputType: 3,
|
|
702
|
+
inputSize: 0,
|
|
703
|
+
inputFields: [],
|
|
704
|
+
outputFields: [{ name: 'state', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
705
|
+
structs: {},
|
|
706
|
+
input: {},
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
expect(result.ok).toBe(false)
|
|
710
|
+
if (!result.ok) {
|
|
711
|
+
expect(result.error.status).toBe(404)
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
test('passes requestData as base64 to live client', async () => {
|
|
716
|
+
let capturedReq: { requestData?: string } = {}
|
|
717
|
+
const live = {
|
|
718
|
+
async querySmartContract(req: unknown) {
|
|
719
|
+
capturedReq = req as { requestData?: string }
|
|
720
|
+
return ok({ responseData: '' })
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
await callContractFunction<{ val: number }, Record<string, never>>({
|
|
725
|
+
live,
|
|
726
|
+
contractIndex: 1,
|
|
727
|
+
inputType: 1,
|
|
728
|
+
inputSize: 4,
|
|
729
|
+
inputFields: [{ name: 'val', type: 'uint32', offset: 0, byteLength: 4 }],
|
|
730
|
+
outputFields: [],
|
|
731
|
+
structs: {},
|
|
732
|
+
input: { val: 42 },
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
expect(capturedReq.requestData).toBeDefined()
|
|
736
|
+
expect(typeof capturedReq.requestData).toBe('string')
|
|
737
|
+
// Base64 decodes to 4 bytes (uint32)
|
|
738
|
+
const decoded = atob(capturedReq.requestData!)
|
|
739
|
+
expect(decoded.length).toBe(4)
|
|
740
|
+
const view = new DataView(new Uint8Array([...decoded].map((c) => c.charCodeAt(0))).buffer)
|
|
741
|
+
expect(view.getUint32(0, true)).toBe(42)
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
// ── Error path: missing required input fields ──
|
|
745
|
+
|
|
746
|
+
test('builder with missing required field still produces payload (no runtime validation)', () => {
|
|
747
|
+
// The buildPayload function from @qubic.org/registry handles missing fields
|
|
748
|
+
// by using default values. This tests that the function doesn't crash.
|
|
749
|
+
const call = buildQearnUnlockInput({ amount: 100n, lockedEpoch: 0 })
|
|
750
|
+
expect(call.payload.byteLength).toBe(12)
|
|
751
|
+
expect(call.contractIndex).toBe(QEARN_CONTRACT_INDEX)
|
|
752
|
+
})
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
// ──────────────────────────────────────────────
|
|
756
|
+
// 9. qbay tests (array fields + struct encoding)
|
|
757
|
+
// ──────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
describe('qbay contract payload encoding', () => {
|
|
760
|
+
test('buildQbaySettingCFBAndQubicPriceInput encodes two uint64 fields', async () => {
|
|
761
|
+
const {
|
|
762
|
+
buildQbaySettingCFBAndQubicPriceInput,
|
|
763
|
+
QBAY_CONTRACT_INDEX,
|
|
764
|
+
QBAY_SETTING_CFB_AND_QUBIC_PRICE_INPUT_TYPE,
|
|
765
|
+
} = (await getContractModule('qbay.js')) as typeof import('../__generated__/qbay.js')
|
|
766
|
+
const call = buildQbaySettingCFBAndQubicPriceInput({ CFBPrice: 100n, QubicPrice: 200n })
|
|
767
|
+
expect(call.contractIndex).toBe(QBAY_CONTRACT_INDEX)
|
|
768
|
+
expect(call.inputType).toBe(QBAY_SETTING_CFB_AND_QUBIC_PRICE_INPUT_TYPE)
|
|
769
|
+
expect(call.payload.byteLength).toBe(16)
|
|
770
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
771
|
+
expect(view.getBigUint64(0, true)).toBe(100n)
|
|
772
|
+
expect(view.getBigUint64(8, true)).toBe(200n)
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
test('buildQbuyBuyInput encodes uint32 + uint8', async () => {
|
|
776
|
+
const { buildQbayBuyInput, QBAY_CONTRACT_INDEX, QBAY_BUY_INPUT_TYPE } =
|
|
777
|
+
(await getContractModule('qbay.js')) as typeof import('../__generated__/qbay.js')
|
|
778
|
+
const call = buildQbayBuyInput({ NFTid: 42, methodOfPayment: 1 })
|
|
779
|
+
expect(call.contractIndex).toBe(QBAY_CONTRACT_INDEX)
|
|
780
|
+
expect(call.inputType).toBe(QBAY_BUY_INPUT_TYPE)
|
|
781
|
+
expect(call.payload.byteLength).toBe(5)
|
|
782
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
783
|
+
expect(view.getUint32(0, true)).toBe(42)
|
|
784
|
+
expect(call.payload[4]).toBe(1)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('decodeQbayBuyOutput decodes returnCode', async () => {
|
|
788
|
+
const { decodeQbayBuyOutput } = (await getContractModule('qbay.js')) as typeof import('../__generated__/qbay.js')
|
|
789
|
+
const data = new Uint8Array(4)
|
|
790
|
+
new DataView(data.buffer).setUint32(0, 0, true)
|
|
791
|
+
expect(decodeQbayBuyOutput(data).returnCode).toBe(0)
|
|
792
|
+
})
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
// ──────────────────────────────────────────────
|
|
796
|
+
// 10. qvault contract tests (complex structs + many functions)
|
|
797
|
+
// ──────────────────────────────────────────────
|
|
798
|
+
|
|
799
|
+
describe('qvault contract payload encoding', () => {
|
|
800
|
+
test('buildQVAULTStakeInput encodes uint32 amount', async () => {
|
|
801
|
+
const { buildQVAULTStakeInput, QVAULT_CONTRACT_INDEX, QVAULT_STAKE_INPUT_TYPE } =
|
|
802
|
+
(await getContractModule('qvault.js')) as typeof import('../__generated__/qvault.js')
|
|
803
|
+
const call = buildQVAULTStakeInput({ amount: 5000 })
|
|
804
|
+
expect(call.contractIndex).toBe(QVAULT_CONTRACT_INDEX)
|
|
805
|
+
expect(call.inputType).toBe(QVAULT_STAKE_INPUT_TYPE)
|
|
806
|
+
expect(call.payload.byteLength).toBe(4)
|
|
807
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
808
|
+
expect(view.getUint32(0, true)).toBe(5000)
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
test('decodeQVAULTStakeOutput decodes returnCode', async () => {
|
|
812
|
+
const { decodeQVAULTStakeOutput } = (await getContractModule('qvault.js')) as typeof import('../__generated__/qvault.js')
|
|
813
|
+
const data = new Uint8Array(4)
|
|
814
|
+
new DataView(data.buffer).setInt32(0, -1, true)
|
|
815
|
+
expect(decodeQVAULTStakeOutput(data).returnCode).toBe(-1)
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
test('buildQVAULTSubmitGPInput encodes 256-byte url array', async () => {
|
|
819
|
+
const { buildQVAULTSubmitGPInput, QVAULT_CONTRACT_INDEX, QVAULT_SUBMIT_GP_INPUT_TYPE } =
|
|
820
|
+
(await getContractModule('qvault.js')) as typeof import('../__generated__/qvault.js')
|
|
821
|
+
const url = new Array(256).fill(0) as number[]
|
|
822
|
+
url[0] = 104 // 'h'
|
|
823
|
+
url[1] = 116 // 't'
|
|
824
|
+
const call = buildQVAULTSubmitGPInput({ url })
|
|
825
|
+
expect(call.contractIndex).toBe(QVAULT_CONTRACT_INDEX)
|
|
826
|
+
expect(call.inputType).toBe(QVAULT_SUBMIT_GP_INPUT_TYPE)
|
|
827
|
+
expect(call.payload.byteLength).toBe(256)
|
|
828
|
+
expect(call.payload[0]).toBe(104)
|
|
829
|
+
expect(call.payload[1]).toBe(116)
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
test('qVAULT namespace has many function references', async () => {
|
|
833
|
+
const { qVAULT } = (await getContractModule('qvault.js')) as typeof import('../__generated__/qvault.js')
|
|
834
|
+
expect(qVAULT.contractIndex).toBe(10)
|
|
835
|
+
expect(typeof qVAULT.getData).toBe('function')
|
|
836
|
+
expect(typeof qVAULT.getStakedAmountAndVotingPower).toBe('function')
|
|
837
|
+
expect(typeof qVAULT.buildStakeInput).toBe('function')
|
|
838
|
+
expect(typeof qVAULT.decodeStakeOutput).toBe('function')
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
// ──────────────────────────────────────────────
|
|
843
|
+
// 11. Encode/decode round-trip for a qearn function
|
|
844
|
+
// ──────────────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
describe('qearn encode/decode round-trip', () => {
|
|
847
|
+
test('buildQearnUnlockInput payload round-trips correctly', () => {
|
|
848
|
+
const call = buildQearnUnlockInput({ amount: 1_000_000n, lockedEpoch: 300 })
|
|
849
|
+
const view = new DataView(call.payload.buffer, call.payload.byteOffset)
|
|
850
|
+
// Read back amount
|
|
851
|
+
const decodedAmount = view.getBigUint64(0, true)
|
|
852
|
+
expect(decodedAmount).toBe(1_000_000n)
|
|
853
|
+
// Read back lockedEpoch
|
|
854
|
+
const decodedEpoch = view.getUint32(8, true)
|
|
855
|
+
expect(decodedEpoch).toBe(300)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
test('decodeQearnLockOutput round-trips negative returnCode', () => {
|
|
859
|
+
const data = new Uint8Array(4)
|
|
860
|
+
new DataView(data.buffer).setInt32(0, -5, true)
|
|
861
|
+
const output = decodeQearnLockOutput(data)
|
|
862
|
+
expect(output.returnCode).toBe(-5)
|
|
863
|
+
})
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
// ──────────────────────────────────────────────
|
|
867
|
+
// 12. Cross-contract consistency tests
|
|
868
|
+
// ──────────────────────────────────────────────
|
|
869
|
+
|
|
870
|
+
describe('cross-contract consistency', () => {
|
|
871
|
+
test('all CONTRACT_INDEX values are unique (except shared indices)', async () => {
|
|
872
|
+
const indices = new Map<number, string[]>()
|
|
873
|
+
for (const meta of CONTRACT_META) {
|
|
874
|
+
const mod = await getContractModule(meta.module)
|
|
875
|
+
const indexKey = Object.keys(mod).find((k) => k.endsWith('_CONTRACT_INDEX'))
|
|
876
|
+
const val = mod[indexKey!] as number
|
|
877
|
+
const existing = indices.get(val) ?? []
|
|
878
|
+
existing.push(meta.name)
|
|
879
|
+
indices.set(val, existing)
|
|
880
|
+
}
|
|
881
|
+
// Known shared indices: msvault + multisignvault both = 11, qip + qubicicoportal both = 18
|
|
882
|
+
for (const [, names] of indices) {
|
|
883
|
+
if (names.length > 1) {
|
|
884
|
+
// These are the known duplicates
|
|
885
|
+
const sorted = [...names].sort()
|
|
886
|
+
expect(
|
|
887
|
+
[
|
|
888
|
+
['msvault', 'multisignvault'],
|
|
889
|
+
['qip', 'qubicicoportal'],
|
|
890
|
+
].some((pair) => pair[0] === sorted[0] && pair[1] === sorted[1]),
|
|
891
|
+
).toBe(true)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
test('all namespace objects have contractIndex property', async () => {
|
|
897
|
+
for (const meta of CONTRACT_META) {
|
|
898
|
+
if (!meta.namespace) continue
|
|
899
|
+
const mod = await getContractModule(meta.module)
|
|
900
|
+
const ns = mod[meta.namespace!] as Record<string, unknown> | undefined
|
|
901
|
+
expect(ns).toBeDefined()
|
|
902
|
+
expect(ns!.contractIndex).toBe(meta.index)
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
test('all builder functions return objects with contractIndex matching module', async () => {
|
|
907
|
+
for (const meta of CONTRACT_META) {
|
|
908
|
+
const mod = await getContractModule(meta.module)
|
|
909
|
+
const buildKeys = Object.keys(mod).filter((k) => k.startsWith('build') && k.endsWith('Input'))
|
|
910
|
+
|
|
911
|
+
for (const key of buildKeys) {
|
|
912
|
+
const fn = mod[key] as ((...args: unknown[]) => unknown) | undefined
|
|
913
|
+
if (typeof fn !== 'function') continue
|
|
914
|
+
|
|
915
|
+
// Try calling with empty input to see if it's a ContractCall builder
|
|
916
|
+
try {
|
|
917
|
+
const result = fn({}) as unknown
|
|
918
|
+
if (
|
|
919
|
+
result &&
|
|
920
|
+
typeof result === 'object' &&
|
|
921
|
+
'contractIndex' in (result as Record<string, unknown>)
|
|
922
|
+
) {
|
|
923
|
+
const cc = result as { contractIndex: number }
|
|
924
|
+
expect(cc.contractIndex).toBe(meta.index)
|
|
925
|
+
}
|
|
926
|
+
// If it returns Uint8Array, that's fine too (function input builder)
|
|
927
|
+
} catch {
|
|
928
|
+
// Needs identityToPublicKey or specific fields; skip
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
})
|