@xyo-network/boundwitness-builder 2.89.2 → 2.90.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/src/Builder.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { toArrayBuffer, toUint8Array } from '@xylabs/arraybuffer'
2
2
  import { assertEx } from '@xylabs/assert'
3
3
  import { Address, Hash, hexFromArrayBuffer } from '@xylabs/hex'
4
- import { JsonObject } from '@xylabs/object'
4
+ import { AnyObject, JsonObject } from '@xylabs/object'
5
5
  import { AccountInstance } from '@xyo-network/account-model'
6
6
  import { BoundWitness, BoundWitnessSchema } from '@xyo-network/boundwitness-model'
7
7
  import { sortFields } from '@xyo-network/hash'
@@ -9,22 +9,22 @@ import { PayloadBuilder, PayloadBuilderBase, PayloadBuilderOptions, PayloadWrapp
9
9
  import { ModuleError, Payload, Schema, WithMeta } from '@xyo-network/payload-model'
10
10
  import { Mutex } from 'async-mutex'
11
11
 
12
- type GeneratedBoundWitnessFields = 'addresses' | 'payload_hashes' | 'payload_schemas' | 'previous_hashes'
12
+ export type GeneratedBoundWitnessFields = 'addresses' | 'payload_hashes' | 'payload_schemas' | 'previous_hashes'
13
13
 
14
- export interface BoundWitnessBuilderOptions<T extends BoundWitness = BoundWitness, TPayload extends Payload = Payload>
15
- extends Omit<PayloadBuilderOptions<Omit<T, GeneratedBoundWitnessFields>>, 'schema'> {
14
+ export interface BoundWitnessBuilderOptions<TBoundWitness extends BoundWitness = BoundWitness, TPayload extends Payload = Payload>
15
+ extends Omit<PayloadBuilderOptions<Omit<TBoundWitness, GeneratedBoundWitnessFields>>, 'schema'> {
16
16
  readonly accounts?: AccountInstance[]
17
17
  readonly destination?: string[]
18
- readonly payloadHashes?: T['payload_hashes']
19
- readonly payloadSchemas?: T['payload_schemas']
18
+ readonly payloadHashes?: TBoundWitness['payload_hashes']
19
+ readonly payloadSchemas?: TBoundWitness['payload_schemas']
20
20
  readonly payloads?: TPayload[]
21
21
  readonly sourceQuery?: Hash
22
- readonly timestamp?: boolean | number
22
+ readonly timestamp?: number
23
23
  }
24
24
 
25
- export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload extends Payload = Payload> extends PayloadBuilderBase<
26
- Omit<T, GeneratedBoundWitnessFields>,
27
- BoundWitnessBuilderOptions<T> & { schema: BoundWitnessSchema }
25
+ export class BoundWitnessBuilder<TBoundWitness extends BoundWitness = BoundWitness, TPayload extends Payload = Payload> extends PayloadBuilderBase<
26
+ Omit<TBoundWitness, GeneratedBoundWitnessFields>,
27
+ BoundWitnessBuilderOptions<TBoundWitness> & { schema: BoundWitnessSchema }
28
28
  > {
29
29
  private static readonly _buildMutex = new Mutex()
30
30
  private _accounts: AccountInstance[]
@@ -37,7 +37,7 @@ export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload
37
37
  private _sourceQuery?: Hash
38
38
  private _timestamp: boolean | number
39
39
 
40
- constructor(options?: BoundWitnessBuilderOptions<T, TPayload>) {
40
+ constructor(options?: BoundWitnessBuilderOptions<TBoundWitness, TPayload>) {
41
41
  super({ ...options, schema: BoundWitnessSchema })
42
42
  const { accounts, payloadHashes, payloadSchemas, payloads, sourceQuery, timestamp, destination } = options ?? {}
43
43
  this._accounts = accounts ?? []
@@ -49,7 +49,11 @@ export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload
49
49
  this._timestamp = timestamp ?? true
50
50
  }
51
51
 
52
- private get _payload_schemas(): string[] {
52
+ protected get addresses(): Address[] {
53
+ return this._accounts.map((account) => account.address)
54
+ }
55
+
56
+ protected get payloadSchemas(): string[] {
53
57
  return (
54
58
  this._payloadSchemas ??
55
59
  this._payloads.map((payload) => {
@@ -58,6 +62,18 @@ export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload
58
62
  )
59
63
  }
60
64
 
65
+ protected get previousHashBuffers(): (ArrayBuffer | null)[] {
66
+ return this._accounts.map((account) => account.previousHashBytes ?? null)
67
+ }
68
+
69
+ protected get previousHashes(): (Hash | null)[] {
70
+ return this._accounts.map((account) => account.previousHash ?? null)
71
+ }
72
+
73
+ protected get timestamp(): number {
74
+ return (this._timestamp = typeof this._timestamp === 'number' ? this._timestamp : Date.now())
75
+ }
76
+
61
77
  static addressIndex<T extends BoundWitness>(payload: T, address: Address) {
62
78
  const index = payload.addresses.indexOf(address)
63
79
  if (index === -1) {
@@ -66,69 +82,107 @@ export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload
66
82
  return index
67
83
  }
68
84
 
69
- static previousHash<T extends BoundWitness>(payload: T, address: Address) {
70
- return payload.previous_hashes[this.addressIndex(payload, address)]
85
+ static async build<TBoundWitness extends BoundWitness>(options: BoundWitnessBuilderOptions<TBoundWitness>) {
86
+ return await new BoundWitnessBuilder(options).build()
71
87
  }
72
88
 
73
- static signature<T extends BoundWitness>(payload: T, address: Address) {
74
- return payload.$meta.signatures[this.addressIndex(payload, address)]
89
+ static override async dataHashableFields<T extends Payload = Payload<AnyObject>>(
90
+ schema: string,
91
+ fields?: Omit<T, 'schema' | '$hash' | '$meta'>,
92
+ ): Promise<Omit<T, '$hash' | '$meta'>> {
93
+ return await PayloadBuilderBase.dataHashableFields(schema, fields)
75
94
  }
76
95
 
77
- async build(): Promise<[WithMeta<T>, TPayload[], ModuleError[]]> {
78
- return await BoundWitnessBuilder._buildMutex.runExclusive(async () => {
79
- const hashableFields = await this.dataHashableFields()
80
- const hash = (await PayloadBuilder.build(hashableFields)).$hash
96
+ static override async hashableFields<T extends Payload = Payload<AnyObject>>(
97
+ schema: string,
98
+ fields?: Omit<T, 'schema' | '$hash' | '$meta'>,
99
+ $meta?: JsonObject,
100
+ $hash?: Hash,
101
+ timestamp?: number,
102
+ ): Promise<WithMeta<T>> {
103
+ return await PayloadBuilderBase.hashableFields(schema, fields, $meta, $hash, timestamp)
104
+ }
81
105
 
82
- /* get all the previousHashes to verify atomic signing */
83
- const previousHashes = this._accounts.map((account) => account.previousHash)
106
+ static previousHash<T extends BoundWitness>(boundWitness: T, address: Address) {
107
+ return boundWitness.previous_hashes[this.addressIndex(boundWitness, address)]
108
+ }
84
109
 
85
- const metaHolder: { $meta?: JsonObject } = {}
110
+ protected static async linkingFields<T extends BoundWitness = BoundWitness>(
111
+ accounts: AccountInstance[],
112
+ payloads?: Payload[],
113
+ timestamp = Date.now(),
114
+ ) {
115
+ const addresses = accounts.map((account) => hexFromArrayBuffer(account.addressBytes, { prefix: false }))
116
+ const previous_hashes = accounts.map((account) => account.previousHash ?? null)
117
+ const payload_hashes = payloads ? await PayloadBuilder.dataHashes(payloads) : []
118
+ const payload_schemas = payloads?.map(({ schema }) => schema)
119
+ return { addresses, payload_hashes, payload_schemas, previous_hashes, timestamp } as Omit<T, '$meta' | '$hash' | 'schema'>
120
+ }
86
121
 
87
- if (hashableFields.addresses.length > 0) {
88
- metaHolder.$meta = metaHolder.$meta ?? {}
89
- metaHolder.$meta.signatures = await this.signatures(hash, previousHashes)
90
- }
122
+ protected static async metaFields(
123
+ dataHash: Hash,
124
+ otherMeta?: JsonObject,
125
+ accounts?: AccountInstance[],
126
+ previousHashes?: (Hash | null)[],
127
+ destination?: Address[],
128
+ sourceQuery?: Hash,
129
+ ): Promise<JsonObject> {
130
+ const meta: JsonObject = { ...otherMeta }
131
+
132
+ if (accounts?.length && previousHashes?.length) {
133
+ assertEx(accounts.length === previousHashes.length, 'accounts and previousHashes must have same length')
134
+ meta.signatures = await this.signatures(accounts, dataHash, previousHashes)
135
+ }
91
136
 
92
- if (this._sourceQuery) {
93
- metaHolder.$meta = metaHolder.$meta ?? {}
94
- metaHolder.$meta.sourceQuery = this._sourceQuery
95
- }
137
+ if (sourceQuery) {
138
+ meta.sourceQuery = sourceQuery
139
+ }
96
140
 
97
- if (this._destination) {
98
- metaHolder.$meta = metaHolder.$meta ?? {}
99
- metaHolder.$meta.destination = this._destination
100
- }
141
+ if (destination) {
142
+ meta.destination = destination
143
+ }
101
144
 
102
- const ret = {
103
- ...hashableFields,
104
- $hash: hash,
105
- ...metaHolder,
106
- } as WithMeta<T>
107
- return [ret, this._payloads, this._errors]
108
- })
145
+ return meta
109
146
  }
110
147
 
111
- override async dataHashableFields(): Promise<Omit<T, '$meta' | '$hash'>> {
112
- const addresses = this._accounts.map((account) => hexFromArrayBuffer(account.addressBytes, { prefix: false }))
113
- const previous_hashes = this._accounts.map((account) => account.previousHash ?? null)
114
- const payload_hashes = assertEx(await this.getPayloadHashes(), 'Missing payload_hashes')
115
- const payload_schemas = assertEx(this._payload_schemas, 'Missing payload_schemas')
116
- const fields = { addresses, payload_hashes, payload_schemas, previous_hashes } as Omit<T, '$meta' | '$hash' | 'schema'>
117
- const result = await BoundWitnessBuilder.dataHashableFields<T>(this._schema, fields)
148
+ protected static signature<T extends BoundWitness>(payload: T, address: Address) {
149
+ return payload.$meta.signatures[this.addressIndex(payload, address)]
150
+ }
118
151
 
119
- assertEx(result.payload_hashes?.length === result.payload_schemas?.length, 'Payload hash/schema mismatch')
152
+ protected static async signatures(accounts: AccountInstance[], hash: Hash, previousHashes: (Hash | ArrayBuffer | null)[]): Promise<string[]> {
153
+ const hashBytes = toArrayBuffer(hash)
154
+ const previousHashesBytes = previousHashes?.map((ph) => (ph ? toUint8Array(ph) : undefined))
155
+ return await Promise.all(accounts.map(async (account, index) => hexFromArrayBuffer(await account.sign(hashBytes, previousHashesBytes[index]))))
156
+ }
120
157
 
121
- assertEx(!result.payload_hashes.some((hash) => !hash), () => 'nulls found in hashes')
158
+ private static validateLinkingFields(bw: Pick<BoundWitness, 'payload_hashes' | 'payload_schemas'>) {
159
+ assertEx(bw.payload_hashes?.length === bw.payload_schemas?.length, 'Payload hash/schema mismatch')
160
+ assertEx(!bw.payload_hashes.some((hash) => !hash), () => 'nulls found in hashes')
161
+ assertEx(!bw.payload_schemas.some((schema) => !schema), 'nulls found in schemas')
162
+ }
122
163
 
123
- assertEx(!result.payload_schemas.some((schema) => !schema), 'nulls found in schemas')
164
+ async build(): Promise<[WithMeta<TBoundWitness>, TPayload[], ModuleError[]]> {
165
+ return await BoundWitnessBuilder._buildMutex.runExclusive(async () => {
166
+ const dataHashableFields = (await this.dataHashableFields()) as TBoundWitness
167
+ const $hash = (await PayloadBuilder.build(dataHashableFields)).$hash
168
+ const $meta = await this.metaFields($hash)
124
169
 
125
- if (typeof this._timestamp === 'number') {
126
- result.timestamp = this._timestamp
127
- } else if (this._timestamp) {
128
- result.timestamp = Date.now()
129
- }
170
+ const ret = {
171
+ ...dataHashableFields,
172
+ $hash,
173
+ $meta,
174
+ } as WithMeta<TBoundWitness>
175
+ return [ret, this._payloads, this._errors]
176
+ })
177
+ }
130
178
 
131
- return result as Omit<T, '$meta' | '$hash'>
179
+ override async dataHashableFields(): Promise<Omit<TBoundWitness, '$meta' | '$hash'>> {
180
+ const fields = await this.linkingFields()
181
+ const result = await BoundWitnessBuilder.dataHashableFields<TBoundWitness>(this._schema, fields)
182
+
183
+ BoundWitnessBuilder.validateLinkingFields(result)
184
+
185
+ return result as Omit<TBoundWitness, '$meta' | '$hash'>
132
186
  }
133
187
 
134
188
  async error(payload?: ModuleError) {
@@ -196,14 +250,18 @@ export class BoundWitnessBuilder<T extends BoundWitness = BoundWitness, TPayload
196
250
  return this
197
251
  }
198
252
 
199
- protected async signatures(_hash: Hash, previousHashes: (Hash | ArrayBuffer | undefined)[]): Promise<string[]> {
253
+ protected async signatures(_hash: Hash, previousHashes: (Hash | ArrayBuffer | null)[]): Promise<string[]> {
200
254
  const hash = toArrayBuffer(_hash)
201
255
  const previousHashesBytes = previousHashes.map((ph) => (ph ? toUint8Array(ph) : undefined))
202
256
  return await Promise.all(this._accounts.map(async (account, index) => hexFromArrayBuffer(await account.sign(hash, previousHashesBytes[index]))))
203
257
  }
204
258
 
205
- private async getPayloadHashes(): Promise<string[]> {
206
- return this._payloadHashes ?? (await Promise.all(this._payloads.map(async (payload) => (await PayloadBuilder.build(payload)).$hash)))
259
+ private async linkingFields() {
260
+ return await BoundWitnessBuilder.linkingFields<TBoundWitness>(this._accounts, this._payloads, this.timestamp)
261
+ }
262
+
263
+ private async metaFields(dataHash: Hash): Promise<JsonObject> {
264
+ return await BoundWitnessBuilder.metaFields(dataHash, this._$meta, this._accounts, this.previousHashes, this._destination, this._sourceQuery)
207
265
  }
208
266
 
209
267
  private missingSchemaMessage(payload: Payload) {