Skip to content

Commit f97c173

Browse files
flotobclaude
andcommitted
feat(swarm): chunk tier + signing identity — SWIP provider API update
Brings the mobile window.swarm provider up to the current SWIP draft (ethersphere/SWIPs#94), matching the desktop implementation (solardev-xyz/freedom-browser#81). The mobile side had only the original ten high-level methods; dapps using the newer surface failed with "Method not supported: swarm_getSigningIdentity". New methods: - swarm_publishChunk / swarm_readChunk (CAC tier, publish permission / permission-free) - swarm_writeSingleOwnerChunk / swarm_readSingleOwnerChunk (SOC tier, feed-permission signing / permission-free) - swarm_getSigningIdentity (feed-permission tier; no-prompt fast path once granted, sheet-bootstrapped first grant) Spec-mandated behaviors that came with the update: - chunk-type validation on reads (BMT recompute for CACs, signature recovery + address re-derivation for SOCs) → chunk_type_mismatch - per-origin rate/bandwidth budgets on the four permission-free read methods (600 req / 5 MB connected, 120 req / 512 KB anonymous, 60 s windows) → rate_limited - structured reasons: invalid_reference, invalid_identifier, invalid_span, chunk_not_found, chunk_type_mismatch, unsupported_option, rate_limited - span as number | bigint across the JS bridge (bigint → decimal string → u64; read replies convert back) - capabilities: maxChunkPayloadBytes (4096), publisherIdentityModes, extensions.publisherSigning - owner fields normalized to EIP-55 checksummed 0x form across createFeed / listFeeds / writeSingleOwnerChunk / getSigningIdentity (spec requires one identical owner string; matches desktop) Verification: 821 unit tests green, including a new end-to-end test that runs swarm-kit's runSwarmProviderCompliance harness inside a real WKWebView against the real preload + bridge + vault signing (12/12 compliance cases pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b487ea8 commit f97c173

26 files changed

Lines changed: 2120 additions & 53 deletions

Freedom/Freedom/BrowserTab.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,17 @@ final class BrowserTab {
265265
} catch BeeAPIClient.Error.notRunning {
266266
throw SwarmRouter.FeedReadError.unreachable
267267
}
268-
}
268+
},
269+
readChunkRaw: { reference in
270+
do {
271+
return try await swarm.bee.getChunk(reference: reference)
272+
} catch BeeAPIClient.Error.notFound {
273+
throw SwarmRouter.ChunkReadError.notFound
274+
} catch BeeAPIClient.Error.notRunning {
275+
throw SwarmRouter.ChunkReadError.unreachable
276+
}
277+
},
278+
readBudget: swarm.readBudget
269279
)
270280
self.swarmBridge = SwarmBridge(
271281
tab: self,

Freedom/Freedom/FreedomApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ struct FreedomApp: App {
143143
bee: swarmBee,
144144
publishService: SwarmPublishService.live(bee: swarmBee),
145145
feedService: SwarmFeedService.live(bee: swarmBee),
146+
chunkService: SwarmChunkService.live(bee: swarmBee),
147+
readBudget: SwarmReadBudget(),
146148
vault: vault,
147149
tagOwnership: TagOwnership(),
148150
feedWriteLock: SwarmFeedWriteLock(),

Freedom/Freedom/Swarm/API/BeeAPIClient.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ struct BeeAPIClient {
228228
return (reference, tagUid)
229229
}
230230

231+
/// `POST /chunks` — uploads a single content-addressed chunk. Body
232+
/// is `span_8LE || payload`; bee recomputes the BMT address and
233+
/// returns it as `reference`. Same pin/deferred headers as the
234+
/// other publish paths (SWIP: chunk uploads MUST be pinned).
235+
func postChunk(
236+
body: Data, batchID: String
237+
) async throws -> (reference: String, tagUid: Int?) {
238+
let (data, responseHeaders) = try await postBytes(
239+
"/chunks",
240+
body: body,
241+
contentType: "application/octet-stream",
242+
headers: [
243+
"Swarm-Postage-Batch-Id": batchID,
244+
"Swarm-Pin": "true",
245+
"Swarm-Deferred-Upload": "true",
246+
]
247+
)
248+
guard let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
249+
let reference = dict["reference"] as? String,
250+
!reference.isEmpty else {
251+
throw Error.malformedResponse
252+
}
253+
let tagUid = responseHeaders["swarm-tag"].flatMap { Int($0) }
254+
return (reference, tagUid)
255+
}
256+
231257
/// `PATCH /stamps/topup/{batchID}/{additionalAmount}` — adds amount
232258
/// to an existing batch's prepayment, extending its TTL. Bee blocks
233259
/// the response until the chain tx confirms (~30 s on Gnosis, but

Freedom/Freedom/Swarm/Bridge/SwarmBridge.js

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,71 @@
6969
}
7070
}
7171

72+
// UTF-8-encode strings, base64 everything — the SWIP's
73+
// `string | Uint8Array | ArrayBuffer` payload contract for
74+
// writeFeedEntry and the chunk methods ("Strings are encoded as
75+
// UTF-8"). Distinct from publishFiles' `bytes: string` allowance,
76+
// where a string is already base64.
77+
function __payloadToBase64(data) {
78+
if (typeof data === 'string') {
79+
return __toBase64(new TextEncoder().encode(data));
80+
}
81+
return __toBase64(data);
82+
}
83+
84+
// Per-method param normalization shared by request() and the
85+
// convenience wrappers, so `request({method, params})` stays
86+
// byte-equivalent to the wrapper call (SWIP §"Convenience Methods").
87+
// Typed arrays / ArrayBuffers become base64 (WKWebView's typed-array
88+
// bridging is version-dependent); `bigint` spans become decimal
89+
// strings (postMessage can't serialize BigInt).
90+
function normalizeParams(method, params) {
91+
params = params || {};
92+
if (method === 'swarm_publishFiles' && Array.isArray(params.files)) {
93+
return Object.assign({}, params, {
94+
files: params.files.map(function (f) {
95+
return {
96+
path: f.path,
97+
contentType: f.contentType,
98+
bytes: __toBase64(f.bytes),
99+
};
100+
}),
101+
});
102+
}
103+
if (method === 'swarm_writeFeedEntry'
104+
&& params.data !== undefined && params.data !== null) {
105+
return Object.assign({}, params, { data: __payloadToBase64(params.data) });
106+
}
107+
if (method === 'swarm_publishChunk' || method === 'swarm_writeSingleOwnerChunk') {
108+
var next = Object.assign({}, params);
109+
if (params.data !== undefined && params.data !== null) {
110+
next.data = __payloadToBase64(params.data);
111+
}
112+
if (typeof params.span === 'bigint') {
113+
next.span = params.span.toString();
114+
}
115+
return next;
116+
}
117+
return params;
118+
}
119+
120+
// Chunk-read spans above Number.MAX_SAFE_INTEGER cross the bridge as
121+
// decimal strings; surface them as `bigint` per the SWIP's
122+
// `span: number | bigint` result contract.
123+
function postProcessResult(method, result) {
124+
if ((method === 'swarm_readChunk' || method === 'swarm_readSingleOwnerChunk')
125+
&& result && typeof result.span === 'string') {
126+
result.span = BigInt(result.span);
127+
}
128+
return result;
129+
}
130+
72131
function makeRequest(method, params) {
73132
const id = ++requestId;
133+
var normalized = normalizeParams(method, params);
74134
return new Promise(function (resolve, reject) {
75-
pendingRequests.set(id, { resolve: resolve, reject: reject });
76-
postToNative({ type: 'request', id: id, method: method, params: params || {} });
135+
pendingRequests.set(id, { resolve: resolve, reject: reject, method: method });
136+
postToNative({ type: 'request', id: id, method: method, params: normalized });
77137
// 5 min ceiling — covers chain-tx-blocked publish/feed-write paths.
78138
// Reads + capability checks resolve in tens of ms, so the cap only
79139
// bites on misbehaving native handlers.
@@ -123,26 +183,16 @@
123183
getUploadStatus: function (params) { return makeRequest('swarm_getUploadStatus', params); },
124184
createFeed: function (params) { return makeRequest('swarm_createFeed', params); },
125185
updateFeed: function (params) { return makeRequest('swarm_updateFeed', params); },
126-
writeFeedEntry: function (params) {
127-
// Normalize `data` to base64 so the native side has one shape
128-
// to decode regardless of whether the dapp passed a string,
129-
// Uint8Array, or ArrayBuffer. Strings are UTF-8-encoded first
130-
// so SOC payload bytes match what the dapp wrote (the SOC
131-
// stores opaque bytes; bee doesn't care about encoding).
132-
var normalized = params;
133-
if (params && params.data !== undefined && params.data !== null) {
134-
var encoded;
135-
if (typeof params.data === 'string') {
136-
encoded = __toBase64(new TextEncoder().encode(params.data));
137-
} else {
138-
encoded = __toBase64(params.data);
139-
}
140-
normalized = Object.assign({}, params, { data: encoded });
141-
}
142-
return makeRequest('swarm_writeFeedEntry', normalized);
143-
},
186+
// Payload/span normalization happens in normalizeParams so the
187+
// request() path behaves identically.
188+
writeFeedEntry: function (params) { return makeRequest('swarm_writeFeedEntry', params); },
144189
readFeedEntry: function (params) { return makeRequest('swarm_readFeedEntry', params); },
145190
listFeeds: function () { return makeRequest('swarm_listFeeds'); },
191+
publishChunk: function (params) { return makeRequest('swarm_publishChunk', params); },
192+
readChunk: function (params) { return makeRequest('swarm_readChunk', params); },
193+
writeSingleOwnerChunk: function (params) { return makeRequest('swarm_writeSingleOwnerChunk', params); },
194+
readSingleOwnerChunk: function (params) { return makeRequest('swarm_readSingleOwnerChunk', params); },
195+
getSigningIdentity: function () { return makeRequest('swarm_getSigningIdentity'); },
146196

147197
on: function (event, handler) {
148198
if (eventListeners[event]) eventListeners[event].push(handler);
@@ -175,7 +225,7 @@
175225
if (error.data) err.data = error.data;
176226
pending.reject(err);
177227
} else {
178-
pending.resolve(result);
228+
pending.resolve(postProcessResult(pending.method, result));
179229
}
180230
},
181231
__handleEvent: function (event, data) {

0 commit comments

Comments
 (0)