@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.
Files changed (163) hide show
  1. package/LICENSE +41 -0
  2. package/README.md +15 -18
  3. package/dist/__generated__/computorcontrolledfund.d.ts +1 -1
  4. package/dist/__generated__/computorcontrolledfund.d.ts.map +1 -1
  5. package/dist/__generated__/computorcontrolledfund.js +8 -8
  6. package/dist/__generated__/computorcontrolledfund.js.map +1 -1
  7. package/dist/__generated__/escrow.d.ts +1 -1
  8. package/dist/__generated__/escrow.d.ts.map +1 -1
  9. package/dist/__generated__/escrow.js +3 -3
  10. package/dist/__generated__/escrow.js.map +1 -1
  11. package/dist/__generated__/generalquorumproposal.d.ts +1 -1
  12. package/dist/__generated__/generalquorumproposal.d.ts.map +1 -1
  13. package/dist/__generated__/generalquorumproposal.js +6 -6
  14. package/dist/__generated__/generalquorumproposal.js.map +1 -1
  15. package/dist/__generated__/ggwp.d.ts +265 -0
  16. package/dist/__generated__/ggwp.d.ts.map +1 -0
  17. package/dist/__generated__/ggwp.js +825 -0
  18. package/dist/__generated__/ggwp.js.map +1 -0
  19. package/dist/__generated__/index.d.ts +1 -0
  20. package/dist/__generated__/index.d.ts.map +1 -1
  21. package/dist/__generated__/index.js +1 -0
  22. package/dist/__generated__/index.js.map +1 -1
  23. package/dist/__generated__/msvault.d.ts +1 -1
  24. package/dist/__generated__/msvault.d.ts.map +1 -1
  25. package/dist/__generated__/msvault.js +19 -19
  26. package/dist/__generated__/msvault.js.map +1 -1
  27. package/dist/__generated__/multisignvault.d.ts +25 -25
  28. package/dist/__generated__/multisignvault.d.ts.map +1 -1
  29. package/dist/__generated__/multisignvault.js +25 -25
  30. package/dist/__generated__/multisignvault.js.map +1 -1
  31. package/dist/__generated__/nostromo.d.ts +1 -1
  32. package/dist/__generated__/nostromo.d.ts.map +1 -1
  33. package/dist/__generated__/nostromo.js +15 -15
  34. package/dist/__generated__/nostromo.js.map +1 -1
  35. package/dist/__generated__/pulse.d.ts +1 -1
  36. package/dist/__generated__/pulse.d.ts.map +1 -1
  37. package/dist/__generated__/pulse.js +26 -26
  38. package/dist/__generated__/pulse.js.map +1 -1
  39. package/dist/__generated__/qbay.d.ts +1 -1
  40. package/dist/__generated__/qbay.d.ts.map +1 -1
  41. package/dist/__generated__/qbay.js +23 -23
  42. package/dist/__generated__/qbay.js.map +1 -1
  43. package/dist/__generated__/qbond.d.ts +1 -1
  44. package/dist/__generated__/qbond.d.ts.map +1 -1
  45. package/dist/__generated__/qbond.js +14 -14
  46. package/dist/__generated__/qbond.js.map +1 -1
  47. package/dist/__generated__/qdraw.d.ts +1 -1
  48. package/dist/__generated__/qdraw.d.ts.map +1 -1
  49. package/dist/__generated__/qdraw.js.map +1 -1
  50. package/dist/__generated__/qduel.d.ts +1 -1
  51. package/dist/__generated__/qduel.d.ts.map +1 -1
  52. package/dist/__generated__/qduel.js +11 -11
  53. package/dist/__generated__/qduel.js.map +1 -1
  54. package/dist/__generated__/qearn.d.ts +1 -1
  55. package/dist/__generated__/qearn.d.ts.map +1 -1
  56. package/dist/__generated__/qearn.js +10 -10
  57. package/dist/__generated__/qearn.js.map +1 -1
  58. package/dist/__generated__/qip.d.ts +18 -1
  59. package/dist/__generated__/qip.d.ts.map +1 -1
  60. package/dist/__generated__/qip.js +64 -3
  61. package/dist/__generated__/qip.js.map +1 -1
  62. package/dist/__generated__/qraffle.d.ts +1 -1
  63. package/dist/__generated__/qraffle.d.ts.map +1 -1
  64. package/dist/__generated__/qraffle.js +12 -12
  65. package/dist/__generated__/qraffle.js.map +1 -1
  66. package/dist/__generated__/qreservepool.d.ts +1 -1
  67. package/dist/__generated__/qreservepool.d.ts.map +1 -1
  68. package/dist/__generated__/qreservepool.js +5 -5
  69. package/dist/__generated__/qreservepool.js.map +1 -1
  70. package/dist/__generated__/qrwa.d.ts +1 -1
  71. package/dist/__generated__/qrwa.d.ts.map +1 -1
  72. package/dist/__generated__/qrwa.js +18 -18
  73. package/dist/__generated__/qrwa.js.map +1 -1
  74. package/dist/__generated__/qswap.d.ts +3 -1
  75. package/dist/__generated__/qswap.d.ts.map +1 -1
  76. package/dist/__generated__/qswap.js +30 -18
  77. package/dist/__generated__/qswap.js.map +1 -1
  78. package/dist/__generated__/qthirtyfour.d.ts +1 -1
  79. package/dist/__generated__/qthirtyfour.d.ts.map +1 -1
  80. package/dist/__generated__/qthirtyfour.js +19 -19
  81. package/dist/__generated__/qthirtyfour.js.map +1 -1
  82. package/dist/__generated__/qubicicoportal.d.ts +4 -4
  83. package/dist/__generated__/qubicicoportal.d.ts.map +1 -1
  84. package/dist/__generated__/qubicicoportal.js +4 -4
  85. package/dist/__generated__/qubicicoportal.js.map +1 -1
  86. package/dist/__generated__/quottery.d.ts +1 -1
  87. package/dist/__generated__/quottery.d.ts.map +1 -1
  88. package/dist/__generated__/quottery.js +14 -14
  89. package/dist/__generated__/quottery.js.map +1 -1
  90. package/dist/__generated__/qusino.d.ts +1 -1
  91. package/dist/__generated__/qusino.d.ts.map +1 -1
  92. package/dist/__generated__/qusino.js +13 -13
  93. package/dist/__generated__/qusino.js.map +1 -1
  94. package/dist/__generated__/qutil.d.ts +1 -1
  95. package/dist/__generated__/qutil.d.ts.map +1 -1
  96. package/dist/__generated__/qutil.js +22 -22
  97. package/dist/__generated__/qutil.js.map +1 -1
  98. package/dist/__generated__/qvault.d.ts +14 -2
  99. package/dist/__generated__/qvault.d.ts.map +1 -1
  100. package/dist/__generated__/qvault.js +70 -32
  101. package/dist/__generated__/qvault.js.map +1 -1
  102. package/dist/__generated__/qx.d.ts +1 -1
  103. package/dist/__generated__/qx.d.ts.map +1 -1
  104. package/dist/__generated__/qx.js +12 -12
  105. package/dist/__generated__/qx.js.map +1 -1
  106. package/dist/__generated__/random.d.ts +24 -1
  107. package/dist/__generated__/random.d.ts.map +1 -1
  108. package/dist/__generated__/random.js +68 -0
  109. package/dist/__generated__/random.js.map +1 -1
  110. package/dist/__generated__/randomlottery.d.ts +1 -1
  111. package/dist/__generated__/randomlottery.d.ts.map +1 -1
  112. package/dist/__generated__/randomlottery.js +12 -12
  113. package/dist/__generated__/randomlottery.js.map +1 -1
  114. package/dist/__generated__/vottunbridge.d.ts +1 -1
  115. package/dist/__generated__/vottunbridge.d.ts.map +1 -1
  116. package/dist/__generated__/vottunbridge.js +13 -13
  117. package/dist/__generated__/vottunbridge.js.map +1 -1
  118. package/dist/call-contract.d.ts.map +1 -1
  119. package/dist/call-contract.js +6 -1
  120. package/dist/call-contract.js.map +1 -1
  121. package/dist/index.d.ts +2 -2
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +1 -1
  124. package/dist/index.js.map +1 -1
  125. package/package.json +7 -6
  126. package/src/__generated__/computorcontrolledfund.ts +1066 -0
  127. package/src/__generated__/escrow.ts +529 -0
  128. package/src/__generated__/generalquorumproposal.ts +691 -0
  129. package/src/__generated__/ggwp.ts +1241 -0
  130. package/src/__generated__/index.ts +28 -0
  131. package/src/__generated__/msvault.ts +1716 -0
  132. package/src/__generated__/multisignvault.ts +1511 -0
  133. package/src/__generated__/mylastmatch.ts +12 -0
  134. package/src/__generated__/nostromo.ts +1551 -0
  135. package/src/__generated__/pulse.ts +1512 -0
  136. package/src/__generated__/qbay.ts +2097 -0
  137. package/src/__generated__/qbond.ts +1016 -0
  138. package/src/__generated__/qdraw.ts +188 -0
  139. package/src/__generated__/qduel.ts +827 -0
  140. package/src/__generated__/qearn.ts +696 -0
  141. package/src/__generated__/qip.ts +501 -0
  142. package/src/__generated__/qraffle.ts +1162 -0
  143. package/src/__generated__/qreservepool.ts +281 -0
  144. package/src/__generated__/qrwa.ts +1491 -0
  145. package/src/__generated__/qswap.ts +1336 -0
  146. package/src/__generated__/qthirtyfour.ts +1131 -0
  147. package/src/__generated__/qubicicoportal.ts +386 -0
  148. package/src/__generated__/quottery.ts +1519 -0
  149. package/src/__generated__/qusino.ts +799 -0
  150. package/src/__generated__/qutil.ts +1506 -0
  151. package/src/__generated__/qvault.ts +2911 -0
  152. package/src/__generated__/qx.ts +861 -0
  153. package/src/__generated__/random.ts +145 -0
  154. package/src/__generated__/randomlottery.ts +653 -0
  155. package/src/__generated__/supplywatcher.ts +12 -0
  156. package/src/__generated__/vottunbridge.ts +1162 -0
  157. package/src/__tests__/contracts.test.ts +933 -0
  158. package/src/call-contract.ts +87 -0
  159. package/src/index.ts +9 -0
  160. package/dist/result.d.ts +0 -12
  161. package/dist/result.d.ts.map +0 -1
  162. package/dist/result.js +0 -7
  163. 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
+ })