@xyo-network/chain-services 1.7.7 → 1.7.9

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.
@@ -0,0 +1,274 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { delay } from '@xylabs/delay'
3
+ import { asAddress } from '@xylabs/hex'
4
+ import { Account } from '@xyo-network/account'
5
+ import type { AccountInstance } from '@xyo-network/account-model'
6
+ import { MemoryArchivist } from '@xyo-network/archivist-memory'
7
+ import type { ArchivistInstance } from '@xyo-network/archivist-model'
8
+ import {
9
+ buildRandomChain, buildRandomTransaction,
10
+ findMostRecentBlock, TestChainId,
11
+ } from '@xyo-network/chain-protocol'
12
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
13
+ import type { WithStorageMeta } from '@xyo-network/payload-model'
14
+ import type {
15
+ BlockBoundWitness, HashPayload,
16
+ HydratedBlock, HydratedTransaction,
17
+ } from '@xyo-network/xl1-protocol'
18
+ import { HashSchema } from '@xyo-network/xl1-protocol'
19
+ import {
20
+ buildTransaction, flattenHydratedBlock, flattenHydratedTransactions,
21
+ getDefaultConfig,
22
+ } from '@xyo-network/xl1-protocol-sdk'
23
+ import {
24
+ beforeAll,
25
+ beforeEach, describe, expect, it,
26
+ } from 'vitest'
27
+
28
+ import type { BasePendingTransactionsServiceParams } from '../BasePendingTransactions.ts'
29
+ import { BasePendingTransactionsService } from '../BasePendingTransactions.ts'
30
+ import { hydratedTransactionToPayloadBundle } from '../hydratedTransactionToPayloadBundle.ts'
31
+
32
+ describe('BasePendingTransactionsService', () => {
33
+ let sut: BasePendingTransactionsService
34
+ let chainArchivist: ArchivistInstance
35
+ let pendingBundledTransactionsArchivist: ArchivistInstance
36
+ let rejectedTransactionsArchivist: ArchivistInstance
37
+ let producer: AccountInstance
38
+ let chain: HydratedBlock[]
39
+ let head: WithStorageMeta<BlockBoundWitness>
40
+ const chainId = TestChainId
41
+ const config = getDefaultConfig()
42
+
43
+ beforeAll(async () => {
44
+ producer = await Account.random()
45
+ })
46
+
47
+ beforeEach(async () => {
48
+ chain = await buildRandomChain(producer, 10)
49
+ chainArchivist = await MemoryArchivist.create({ account: 'random' })
50
+ await chainArchivist.insert(chain.flatMap(block => flattenHydratedBlock(block)))
51
+ pendingBundledTransactionsArchivist = await MemoryArchivist.create({ account: 'random' })
52
+ rejectedTransactionsArchivist = await MemoryArchivist.create({ account: 'random' })
53
+ head = assertEx(await findMostRecentBlock(chainArchivist))
54
+ const params: BasePendingTransactionsServiceParams = {
55
+ name: 'TestBasePendingTransactionsServiceParams',
56
+ chainArchivist,
57
+ chainId,
58
+ config,
59
+ pendingBundledTransactionsArchivist,
60
+ rejectedTransactionsArchivist,
61
+ }
62
+ sut = await BasePendingTransactionsService.create(params)
63
+ })
64
+
65
+ describe('with pending transactions', () => {
66
+ let newTransactions: HydratedTransaction[] = []
67
+ beforeEach(async () => {
68
+ for (let index = 0; index < 5; index++) {
69
+ const tx = await buildRandomTransaction(TestChainId)
70
+ newTransactions.push(tx)
71
+ await delay(2)
72
+ }
73
+ const payloads = newTransactions.map(hydratedTransactionToPayloadBundle)
74
+ await pendingBundledTransactionsArchivist.insert(payloads)
75
+ })
76
+ it('should return the requested amount of hydrated transactions', async () => {
77
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
78
+ expect(result.length).toEqual(newTransactions.length)
79
+ })
80
+ })
81
+
82
+ describe('with no pending transactions', () => {
83
+ it('should return an empty array', async () => {
84
+ const result = await sut.getPendingTransactions(head._dataHash, 10)
85
+ expect(result).toEqual([])
86
+ })
87
+ })
88
+
89
+ describe('with pending and finalized transactions', () => {
90
+ let finalizedTransactions: HydratedTransaction[] = []
91
+ let newTransactions: HydratedTransaction[] = []
92
+ beforeEach(async () => {
93
+ for (let index = 0; index < 5; index++) {
94
+ const tx = await buildRandomTransaction(TestChainId)
95
+ finalizedTransactions.push(tx)
96
+ await delay(2)
97
+ }
98
+ await pendingBundledTransactionsArchivist.insert(finalizedTransactions.map(hydratedTransactionToPayloadBundle))
99
+ await chainArchivist.insert(flattenHydratedTransactions(finalizedTransactions))
100
+ for (let index = 0; index < 5; index++) {
101
+ const tx = await buildRandomTransaction(TestChainId)
102
+ newTransactions.push(tx)
103
+ await delay(2)
104
+ }
105
+ const pending = newTransactions.map(hydratedTransactionToPayloadBundle)
106
+ await pendingBundledTransactionsArchivist.insert(pending)
107
+ })
108
+ it('should remove finalized transactions from pending', async () => {
109
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length + finalizedTransactions.length)
110
+ expect(result.length).toEqual(newTransactions.length)
111
+ })
112
+ })
113
+
114
+ describe('with repeated transactions', () => {
115
+ const hashPayload = new PayloadBuilder<HashPayload>({ schema: HashSchema })
116
+ .fields({ hash: '3fc6fb2edd79e0d3f27c668a706cb48473d5f9e432e5462ae647702221a2c97a' })
117
+ .build()
118
+ beforeEach(async () => {})
119
+ it('when unique should allow transactions', async () => {
120
+ let newTransactions: HydratedTransaction[] = []
121
+ // NOTE: Cast to Account to allow loadPreviousHash to be called
122
+ const signer = await Account.random()
123
+ const transaction1 = await buildTransaction(TestChainId, [hashPayload], [], signer, 0, 100)
124
+ const transaction2 = await buildTransaction(TestChainId, [hashPayload], [], signer, 0, 100)
125
+ expect(transaction1[0]._hash).not.toEqual(transaction2[0]._hash)
126
+ for (const tx of [transaction1, transaction2]) {
127
+ const bundled = hydratedTransactionToPayloadBundle(tx)
128
+ await pendingBundledTransactionsArchivist.insert([bundled])
129
+ newTransactions.push(tx)
130
+ }
131
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
132
+ expect(result.length).toEqual(newTransactions.length)
133
+ })
134
+ it('when non-unique should filter duplicate transactions', async () => {
135
+ let newTransactions: HydratedTransaction[] = []
136
+ const previousHash = 'bb27dd470ccf91665d573a16b4f652a47b8a24686d1b44c189c1fa826cd3fef5'
137
+ // NOTE: Cast to Account to allow loadPreviousHash to be called
138
+ const signer = await Account.random() as Account
139
+ await signer.loadPreviousHash(previousHash)
140
+ const transaction1 = await buildTransaction(TestChainId, [hashPayload], [], signer, 0, 100)
141
+ await signer.loadPreviousHash(previousHash)
142
+ const transaction2 = await buildTransaction(TestChainId, [hashPayload], [], signer, 0, 100)
143
+ expect(transaction1[0]._hash).toEqual(transaction2[0]._hash)
144
+ for (const tx of [transaction1, transaction2]) {
145
+ const bundled = hydratedTransactionToPayloadBundle(tx)
146
+ await pendingBundledTransactionsArchivist.insert([bundled])
147
+ newTransactions.push(tx)
148
+ }
149
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
150
+ expect(result.length).toEqual(newTransactions.length / 2)
151
+ })
152
+ })
153
+
154
+ describe('with expiring transactions', () => {
155
+ const insertTransaction = async (nbf: number, exp: number) => {
156
+ const newTransactions: HydratedTransaction[] = []
157
+ const signer = await Account.random()
158
+ const hashPayload = new PayloadBuilder<HashPayload>({ schema: HashSchema })
159
+ .fields({ hash: head._hash })
160
+ .build()
161
+ const tx = await buildRandomTransaction(TestChainId, [hashPayload], signer, nbf, exp)
162
+ const bundled = hydratedTransactionToPayloadBundle(tx)
163
+ await pendingBundledTransactionsArchivist.insert([bundled])
164
+ newTransactions.push(tx)
165
+ await delay(2)
166
+ return newTransactions
167
+ }
168
+ describe('transaction nbf', () => {
169
+ describe('is included', () => {
170
+ it('when equal to previous block', async () => {
171
+ const nbf = head.block - 1
172
+ const exp = head.block + 3
173
+ const newTransactions = await insertTransaction(nbf, exp)
174
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
175
+ expect(result.length).toEqual(newTransactions.length)
176
+ })
177
+ it('when equal to current block', async () => {
178
+ const nbf = head.block
179
+ const exp = head.block + 3
180
+ const newTransactions = await insertTransaction(nbf, exp)
181
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
182
+ expect(result.length).toEqual(newTransactions.length)
183
+ })
184
+ it('when equal to next block', async () => {
185
+ const nbf = head.block + 1
186
+ const exp = head.block + 3
187
+ const newTransactions = await insertTransaction(nbf, exp)
188
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
189
+ expect(result.length).toEqual(newTransactions.length)
190
+ })
191
+ })
192
+ describe('is excluded', () => {
193
+ it('when beyond next block', async () => {
194
+ const nbf = head.block + 2
195
+ const exp = head.block + 3
196
+ const newTransactions = await insertTransaction(nbf, exp)
197
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
198
+ expect(result.length).toEqual(0)
199
+ })
200
+ })
201
+ })
202
+ describe('transaction exp', () => {
203
+ describe('is excluded', () => {
204
+ it('when equal to previous block', async () => {
205
+ const nbf = head.block - 3
206
+ const exp = head.block - 1
207
+ const newTransactions = await insertTransaction(nbf, exp)
208
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
209
+ expect(result.length).toEqual(0)
210
+ })
211
+ it('when equal to current block', async () => {
212
+ const nbf = head.block - 3
213
+ const exp = head.block
214
+ const newTransactions = await insertTransaction(nbf, exp)
215
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
216
+ expect(result.length).toEqual(0)
217
+ })
218
+ })
219
+ describe('is included', () => {
220
+ it('when equal to next block', async () => {
221
+ const nbf = head.block - 3
222
+ const exp = head.block + 1
223
+ const newTransactions = await insertTransaction(nbf, exp)
224
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
225
+ expect(result.length).toEqual(newTransactions.length)
226
+ })
227
+ it('when beyond next block', async () => {
228
+ const nbf = head.block - 3
229
+ const exp = head.block + 3
230
+ const newTransactions = await insertTransaction(nbf, exp)
231
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length)
232
+ expect(result.length).toEqual(newTransactions.length)
233
+ })
234
+ })
235
+ })
236
+ })
237
+
238
+ describe('with pending and rejected transactions', () => {
239
+ let rejectedTransactions: HydratedTransaction[] = []
240
+ let newTransactions: HydratedTransaction[] = []
241
+ beforeEach(async () => {
242
+ for (let index = 0; index < 5; index++) {
243
+ const tx = await buildRandomTransaction(TestChainId)
244
+ rejectedTransactions.push(tx)
245
+ await delay(2)
246
+ }
247
+ await pendingBundledTransactionsArchivist.insert(rejectedTransactions.map(hydratedTransactionToPayloadBundle))
248
+ for (let index = 0; index < 5; index++) {
249
+ const tx = await buildRandomTransaction(TestChainId)
250
+ newTransactions.push(tx)
251
+ await delay(2)
252
+ }
253
+ await pendingBundledTransactionsArchivist.insert(newTransactions.map(hydratedTransactionToPayloadBundle))
254
+ await rejectedTransactionsArchivist.insert(flattenHydratedTransactions(rejectedTransactions))
255
+ })
256
+ it('should remove rejected transactions from pending', async () => {
257
+ const result = await sut.getPendingTransactions(head._hash, newTransactions.length + rejectedTransactions.length)
258
+ expect(result.length).toEqual(newTransactions.length)
259
+ })
260
+ })
261
+
262
+ describe('with invalid transactions in mempool', () => {
263
+ beforeEach(async () => {
264
+ const incorrectChainId = assertEx(asAddress('1234567890abcdef1234567890abcdef12345678'))
265
+ const invalidTransactions: HydratedTransaction[] = [await buildRandomTransaction(incorrectChainId)]
266
+ const pending = invalidTransactions.map(hydratedTransactionToPayloadBundle)
267
+ await pendingBundledTransactionsArchivist.insert(pending)
268
+ })
269
+ it('should remove invalid transactions from pending', async () => {
270
+ const result = await sut.getPendingTransactions(head._hash, 20)
271
+ expect(result.length).toEqual(0)
272
+ })
273
+ })
274
+ })
@@ -0,0 +1,35 @@
1
+ import { buildRandomTransaction, TestChainId } from '@xyo-network/chain-protocol'
2
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
3
+ import type { HashPayload } from '@xyo-network/xl1-protocol'
4
+ import { HashSchema } from '@xyo-network/xl1-protocol'
5
+ import {
6
+ describe, expect, it,
7
+ } from 'vitest'
8
+
9
+ import { bundledPayloadToHydratedTransaction } from '../bundledPayloadToHydratedTransaction.ts'
10
+ import { hydratedTransactionToPayloadBundle } from '../hydratedTransactionToPayloadBundle.ts'
11
+
12
+ describe('bundledPayloadToHydratedTransaction', () => {
13
+ it('creates a hydrated transaction from valid payload bundle', async () => {
14
+ // Create a minimal Transaction
15
+ const hashPayload = new PayloadBuilder<HashPayload>({ schema: HashSchema })
16
+ .fields({ hash: '3fc6fb2edd79e0d3f27c668a706cb48473d5f9e432e5462ae647702221a2c97a' })
17
+ .build()
18
+ const payloads = [hashPayload]
19
+ const tx = await buildRandomTransaction(TestChainId, payloads)
20
+
21
+ // Create bundle
22
+ const bundle = await PayloadBuilder.addStorageMeta(hydratedTransactionToPayloadBundle(tx))
23
+
24
+ // Act
25
+ const result = await bundledPayloadToHydratedTransaction(bundle)
26
+
27
+ // Assert
28
+ expect(result).toBeDefined()
29
+ expect(result?.[0]._hash).toEqual(tx[0]._hash)
30
+ expect(result?.[1].length).toEqual(payloads.length)
31
+ for (const payload of result?.[1] ?? []) {
32
+ expect(payload._hash).toEqual(await PayloadBuilder.hash(payload))
33
+ }
34
+ })
35
+ })
@@ -0,0 +1,28 @@
1
+ import { buildRandomTransaction, TestChainId } from '@xyo-network/chain-protocol'
2
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
3
+ import { asOptionalPayloadBundle } from '@xyo-network/payload-model'
4
+ import type { HashPayload } from '@xyo-network/xl1-protocol'
5
+ import { HashSchema } from '@xyo-network/xl1-protocol'
6
+ import {
7
+ describe, expect, it,
8
+ } from 'vitest'
9
+
10
+ import { hydratedTransactionToPayloadBundle } from '../hydratedTransactionToPayloadBundle.ts'
11
+
12
+ describe('hydratedTransactionToPayloadBundle', () => {
13
+ it('creates a payload bundle from a hydrated transaction', async () => {
14
+ // Create a minimal Transaction
15
+ const hashPayload = new PayloadBuilder<HashPayload>({ schema: HashSchema })
16
+ .fields({ hash: '3fc6fb2edd79e0d3f27c668a706cb48473d5f9e432e5462ae647702221a2c97a' })
17
+ .build()
18
+ const payloads = [hashPayload]
19
+ const tx = await buildRandomTransaction(TestChainId, payloads)
20
+
21
+ // Act
22
+ const bundle = hydratedTransactionToPayloadBundle(tx)
23
+
24
+ // Assert
25
+ expect(bundle).toBeDefined()
26
+ expect(asOptionalPayloadBundle(bundle)).toBeDefined()
27
+ })
28
+ })
@@ -4,7 +4,7 @@ import { creatable } from '@xylabs/creatable'
4
4
  import {
5
5
  Address, asAddress, Hash,
6
6
  } from '@xylabs/hex'
7
- import { isDefined, isUndefined } from '@xylabs/typeof'
7
+ import { isUndefined } from '@xylabs/typeof'
8
8
  import { ArchivistInstance, ArchivistNextOptions } from '@xyo-network/archivist-model'
9
9
  import {
10
10
  analyzeChain, ChainStakeIntentAnalyzer,
package/xy.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { XyTsupConfig } from '@xylabs/ts-scripts-yarn3'
2
- const config: XyTsupConfig = {
3
- compile: {
4
- browser: {},
5
- node: {},
6
- neutral: { src: true },
7
- },
8
- }
9
-
10
- export default config