Skip to content

Commit ae7d4ac

Browse files
feat: improve soc developer api (#1123)
* feat: improve soc developer api * test: update test coverage * refactor: soc qol * test: update test coverage * feat: add unmarshalContentAddressedChunk * style: eslint * test: update test coverage * test: update test coverage * fix: unmarshal wrapped cac in soc * test: update test coverage * fix: unmarshal wrapped cac in soc * test: update test coverage * docs: revert misleading comment * fix: add span to soc * test: update test coverage * fix: update SOC size validation to include Identifier length * test: update test coverage * test: add soc tests * test: update test coverage * fix: keep span when unmarshaling * test: update test coverage * fix: pass span * test: update test coverage * test: update test coverage --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent fed2157 commit ae7d4ac

9 files changed

Lines changed: 277 additions & 95 deletions

File tree

cheatsheet.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ async function main() {
185185
onError: error => {
186186
console.error(error)
187187
},
188+
onClose: () => {
189+
console.log('subscription closed')
190+
},
188191
})
189192
}
190193

@@ -227,6 +230,9 @@ async function main() {
227230
onError: error => {
228231
console.error(error)
229232
},
233+
onClose: () => {
234+
console.log('subscription closed')
235+
},
230236
})
231237
}
232238

src/bee.ts

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { Binary, Objects, System, Types } from 'cafe-utility'
22
import { Readable } from 'stream'
3-
import { Chunk, makeContentAddressedChunk } from './chunk/cac'
4-
import { downloadSingleOwnerChunk, makeSOCAddress, makeSingleOwnerChunk, uploadSingleOwnerChunkData } from './chunk/soc'
3+
import { Chunk, makeContentAddressedChunk, unmarshalContentAddressedChunk } from './chunk/cac'
4+
import {
5+
SingleOwnerChunk,
6+
downloadSingleOwnerChunk,
7+
makeSOCAddress,
8+
makeSingleOwnerChunk,
9+
unmarshalSingleOwnerChunk,
10+
uploadSingleOwnerChunkData,
11+
} from './chunk/soc'
512
import { makeFeedReader, makeFeedWriter } from './feed'
613
import { areAllSequentialFeedsUpdateRetrievable } from './feed/retrievable'
714
import * as bytes from './modules/bytes'
@@ -127,6 +134,7 @@ import {
127134
PrivateKey,
128135
PublicKey,
129136
Reference,
137+
Signature,
130138
Span,
131139
Topic,
132140
TransactionId,
@@ -316,7 +324,7 @@ export class Bee {
316324
* Chunks uploaded with this method should be retrieved with the {@link downloadChunk} method.
317325
*
318326
* @param stamp Postage Batch ID or an Envelope created with the {@link createEnvelope} method.
319-
* @param data Raw chunk to be uploaded
327+
* @param data Raw chunk to be uploaded (Content Addressed Chunk or Single Owner Chunk)
320328
* @param options Additional options like tag, encryption, pinning, content-type and request options
321329
* @param requestOptions Options for making requests, such as timeouts, custom HTTP agents, headers, etc.
322330
*
@@ -326,10 +334,12 @@ export class Bee {
326334
*/
327335
async uploadChunk(
328336
stamp: EnvelopeWithBatchId | BatchId | Uint8Array | string,
329-
data: Uint8Array | Chunk,
337+
data: Uint8Array | Chunk | SingleOwnerChunk,
330338
options?: UploadOptions,
331339
requestOptions?: BeeRequestOptions,
332340
): Promise<UploadResult> {
341+
const isSOC = 'identifier' in data && 'signature' in data && 'owner' in data
342+
333343
data = data instanceof Uint8Array ? data : data.data
334344

335345
if (options) {
@@ -340,8 +350,15 @@ export class Bee {
340350
throw new BeeArgumentError(`Chunk has to have size of at least ${Span.LENGTH}.`, data)
341351
}
342352

343-
if (data.length > CHUNK_SIZE + Span.LENGTH) {
344-
throw new BeeArgumentError(`Chunk has to have size of at most ${CHUNK_SIZE + Span.LENGTH}.`, data)
353+
if (!isSOC && data.length > CHUNK_SIZE + Span.LENGTH) {
354+
throw new BeeArgumentError(`Content Addressed Chunk must not exceed ${CHUNK_SIZE + Span.LENGTH} bytes.`, data)
355+
}
356+
357+
if (isSOC && data.length > CHUNK_SIZE + Span.LENGTH + Signature.LENGTH + Identifier.LENGTH) {
358+
throw new BeeArgumentError(
359+
`Single Owner Chunk must not exceed ${CHUNK_SIZE + Span.LENGTH + Signature.LENGTH + Identifier.LENGTH} bytes.`,
360+
data,
361+
)
345362
}
346363

347364
return chunk.upload(this.getRequestOptionsForCall(requestOptions), data, stamp, options)
@@ -1184,7 +1201,7 @@ export class Bee {
11841201
identifier = new Identifier(identifier)
11851202

11861203
const cac = makeContentAddressedChunk(data)
1187-
const soc = makeSingleOwnerChunk(cac, identifier, signer)
1204+
const soc = cac.toSingleOwnerChunk(identifier, signer)
11881205

11891206
return gsoc.send(this.getRequestOptionsForCall(requestOptions), soc, postageBatchId, options)
11901207
}
@@ -1357,6 +1374,81 @@ export class Bee {
13571374
return fetchLatestFeedUpdate(this.getRequestOptionsForCall(requestOptions), owner, topic)
13581375
}
13591376

1377+
/**
1378+
* Creates a Content Addressed Chunk.
1379+
*
1380+
* To be uploaded with the {@link uploadChunk} method.
1381+
*
1382+
* Payload size must be between 1 and 4096 bytes.
1383+
*
1384+
* @param rawPayload Data to be stored in the chunk. If the data is a string, it will be converted to UTF-8 bytes.
1385+
* @param span Optional span for the chunk. If not provided, it will be set to the length of the payload.
1386+
*
1387+
* @example
1388+
*
1389+
*/
1390+
makeContentAddressedChunk(rawPayload: Bytes | Uint8Array | string, span?: Span | bigint): Chunk {
1391+
return makeContentAddressedChunk(rawPayload, span)
1392+
}
1393+
1394+
/**
1395+
* Attempts to unmarshal arbitrary data into a Content Addressed Chunk.
1396+
* Throws an error if the data is not a valid CAC.
1397+
*
1398+
* @param data The chunk data (`span` and `payload`)
1399+
*/
1400+
unmarshalContentAddressedChunk(data: Bytes | Uint8Array): Chunk {
1401+
return unmarshalContentAddressedChunk(data)
1402+
}
1403+
1404+
/**
1405+
* Creates a Single Owner Chunk.
1406+
*
1407+
* To be uploaded with the {@link uploadChunk} method.
1408+
*
1409+
* Identical to chaining `makeContentAddressedChunk` and `toSingleOwnerChunk`.
1410+
*
1411+
* Payload size must be between 1 and 4096 bytes.
1412+
*
1413+
* @param address Address of the Content Addressed Chunk
1414+
* @param span Span of the Content Addressed Chunk
1415+
* @param payload Payload of the Content Addressed Chunk
1416+
* @param identifier The identifier of the chunk
1417+
* @param signer The signer interface for signing the chunk
1418+
*/
1419+
makeSingleOwnerChunk(
1420+
address: Reference,
1421+
span: Span,
1422+
payload: Bytes,
1423+
identifier: Identifier | Uint8Array | string,
1424+
signer: PrivateKey | Uint8Array | string,
1425+
): SingleOwnerChunk {
1426+
return makeSingleOwnerChunk(address, span, payload, identifier, signer)
1427+
}
1428+
1429+
/**
1430+
* Calculates the address of a Single Owner Chunk based on its identifier and owner address.
1431+
*
1432+
* @param identifier
1433+
* @param address
1434+
*/
1435+
calculateSingleOwnerChunkAddress(identifier: Identifier, address: EthAddress): Reference {
1436+
return makeSOCAddress(identifier, address)
1437+
}
1438+
1439+
/**
1440+
* Attempts to unmarshal arbitrary data into a Single Owner Chunk.
1441+
* Throws an error if the data is not a valid SOC.
1442+
*
1443+
* @param data The chunk data
1444+
* @param address The address of the single owner chunk
1445+
*
1446+
* @returns a single owner chunk or throws error
1447+
*/
1448+
unmarshalSingleOwnerChunk(data: Bytes | Uint8Array, address: Reference | Uint8Array | string): SingleOwnerChunk {
1449+
return unmarshalSingleOwnerChunk(data, address)
1450+
}
1451+
13601452
/**
13611453
* Returns an object for reading single owner chunks
13621454
*

src/chunk/cac.ts

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,80 @@
1-
import { Binary } from 'cafe-utility'
1+
import { Binary, Types } from 'cafe-utility'
22
import { Bytes } from '../utils/bytes'
3-
import { Reference, Span } from '../utils/typed-bytes'
3+
import { Identifier, PrivateKey, Reference, Span } from '../utils/typed-bytes'
44
import { calculateChunkAddress } from './bmt'
5+
import { makeSingleOwnerChunk, SingleOwnerChunk } from './soc'
56

67
export const MIN_PAYLOAD_SIZE = 1
78
export const MAX_PAYLOAD_SIZE = 4096
89

9-
const ENCODER = new TextEncoder()
10-
1110
/**
12-
* General chunk interface for Swarm
11+
* Content Addressed Chunk (CAC) is the immutable building block of Swarm,
12+
* holding at most 4096 bytes of payload.
1313
*
14-
* It stores the serialized data and provides functions to access
15-
* the fields of a chunk.
14+
* - `span` indicates the size of the `payload` in bytes.
15+
* - `payload` contains the actual data or the body of the chunk.
16+
* - `data` contains the full chunk data - `span` and `payload`.
17+
* - `address` is the Swarm hash (or reference) of the chunk.
1618
*
17-
* It also provides an address function to calculate the address of
18-
* the chunk that is required for the Chunk API.
19+
* The `toSingleOwnerChunk` method allows converting the CAC into a Single Owner Chunk (SOC).
1920
*/
2021
export interface Chunk {
22+
/**
23+
* Contains the full chunk data - `span` + `payload`.
24+
*/
2125
readonly data: Uint8Array
26+
/**
27+
* Indicates the size of the `payload` in bytes.
28+
*/
2229
span: Span
30+
/**
31+
* Contains the actual data or the body of the chunk.
32+
*/
2333
payload: Bytes
34+
/**
35+
* The Swarm hash (or reference) of the chunk.
36+
*/
2437
address: Reference
38+
/**
39+
* Converts the CAC into a Single Owner Chunk (SOC).
40+
*/
41+
toSingleOwnerChunk: (
42+
identifier: Identifier | Uint8Array | string,
43+
signer: PrivateKey | Uint8Array | string,
44+
) => SingleOwnerChunk
2545
}
2646

27-
/**
28-
* Creates a content addressed chunk and verifies the payload size.
29-
*
30-
* @param payloadBytes the data to be stored in the chunk
31-
*/
32-
export function makeContentAddressedChunk(payloadBytes: Uint8Array | string): Chunk {
33-
if (!(payloadBytes instanceof Uint8Array)) {
34-
payloadBytes = ENCODER.encode(payloadBytes)
35-
}
47+
export function unmarshalContentAddressedChunk(data: Bytes | Uint8Array): Chunk {
48+
data = new Bytes(data)
3649

37-
if (payloadBytes.length < MIN_PAYLOAD_SIZE || payloadBytes.length > MAX_PAYLOAD_SIZE) {
38-
throw new RangeError(
39-
`payload size ${payloadBytes.length} exceeds limits [${MIN_PAYLOAD_SIZE}, ${MAX_PAYLOAD_SIZE}]`,
40-
)
41-
}
42-
43-
const span = Span.fromBigInt(BigInt(payloadBytes.length))
44-
const data = Binary.concatBytes(span.toUint8Array(), payloadBytes)
50+
return makeContentAddressedChunk(data.toUint8Array().slice(Span.LENGTH), Span.fromSlice(data.toUint8Array(), 0))
51+
}
4552

46-
return {
47-
data,
48-
span,
49-
payload: Bytes.fromSlice(data, Span.LENGTH),
50-
address: calculateChunkAddress(data),
53+
export function makeContentAddressedChunk(rawPayload: Bytes | Uint8Array | string, span?: Span | bigint): Chunk {
54+
if (Types.isString(rawPayload)) {
55+
rawPayload = Bytes.fromUtf8(rawPayload)
5156
}
52-
}
5357

54-
export function asContentAddressedChunk(chunkBytes: Uint8Array): Chunk {
55-
if (chunkBytes.length < MIN_PAYLOAD_SIZE + Span.LENGTH || chunkBytes.length > MAX_PAYLOAD_SIZE + Span.LENGTH) {
56-
throw new RangeError(
57-
`chunk size ${chunkBytes.length} exceeds limits [${MIN_PAYLOAD_SIZE + Span.LENGTH}, ${Span.LENGTH}]`,
58-
)
58+
if (rawPayload.length < MIN_PAYLOAD_SIZE || rawPayload.length > MAX_PAYLOAD_SIZE) {
59+
throw new RangeError(`payload size ${rawPayload.length} exceeds limits [${MIN_PAYLOAD_SIZE}, ${MAX_PAYLOAD_SIZE}]`)
5960
}
6061

61-
const span = Span.fromSlice(chunkBytes, 0)
62-
const data = Binary.concatBytes(span.toUint8Array(), chunkBytes.slice(Span.LENGTH))
62+
const typedSpan: Span = span
63+
? typeof span === 'bigint'
64+
? Span.fromBigInt(span)
65+
: span
66+
: Span.fromBigInt(BigInt(rawPayload.length))
67+
const payload = new Bytes(rawPayload)
68+
const data = Binary.concatBytes(typedSpan.toUint8Array(), payload.toUint8Array())
69+
const address = calculateChunkAddress(data)
6370

6471
return {
6572
data,
66-
span,
67-
payload: Bytes.fromSlice(data, Span.LENGTH),
68-
address: calculateChunkAddress(data),
73+
span: typedSpan,
74+
payload,
75+
address,
76+
toSingleOwnerChunk: (identifier: Identifier | Uint8Array | string, signer: PrivateKey | Uint8Array | string) => {
77+
return makeSingleOwnerChunk(address, typedSpan, payload, identifier, signer)
78+
},
6979
}
7080
}

0 commit comments

Comments
 (0)