@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.
- package/README.md +8669 -3
- package/dist/neutral/index.mjs +1 -1
- package/dist/neutral/index.mjs.map +1 -1
- package/package.json +43 -41
- package/src/BlockProducer/BaseBlockProducerService.ts +1 -1
- package/src/BlockProducer/spec/BaseBlockProducerService.spec.ts +320 -0
- package/src/BlockProducer/spec/generateTransactionTransfer.spec.ts +72 -0
- package/src/PendingTransactions/spec/BasePendingTransactions.spec.ts +274 -0
- package/src/PendingTransactions/spec/bundledPayloadToHydratedTransaction.spec.ts +35 -0
- package/src/PendingTransactions/spec/hydratedTransactionToPayloadBundle.spec.ts +28 -0
- package/src/StakeIntent/XyoStakeIntentService.ts +1 -1
- package/xy.config.ts +0 -10
|
@@ -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 {
|
|
7
|
+
import { isUndefined } from '@xylabs/typeof'
|
|
8
8
|
import { ArchivistInstance, ArchivistNextOptions } from '@xyo-network/archivist-model'
|
|
9
9
|
import {
|
|
10
10
|
analyzeChain, ChainStakeIntentAnalyzer,
|