gethstorage-sdk 5.0.2 → 5.0.3
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 +208 -208
- package/dist/browser/index.d.mts +41 -1
- package/dist/browser/index.mjs +61 -4
- package/dist/node/index.cjs +61 -4
- package/dist/node/index.d.mts +41 -1
- package/dist/node/index.d.ts +41 -1
- package/dist/node/index.mjs +61 -4
- package/package.json +65 -65
package/README.md
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
# gethstorage-sdk
|
|
2
|
-
|
|
3
|
-
This SDK aims to standardize the interaction between applications and the EthStorage network to achieve reliable and
|
|
4
|
-
efficient data management functionality.
|
|
5
|
-
|
|
6
|
-
This SDK provides the `FlatDirectory` class, a higher-level data management tool that provides methods for uploading
|
|
7
|
-
and downloading data of arbitrary size. Uploads are performed by sending the data through calldata.
|
|
8
|
-
|
|
9
|
-
Click here to view [spec](./spec.md).
|
|
10
|
-
|
|
11
|
-
# Installation
|
|
12
|
-
|
|
13
|
-
Install the SDK using [npm](https://www.npmjs.com/package/gethstorage-sdk):
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
$ npm install gethstorage-sdk
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
# Example Usage
|
|
20
|
-
|
|
21
|
-
## FlatDirectory
|
|
22
|
-
|
|
23
|
-
### create
|
|
24
|
-
|
|
25
|
-
Create a `FlatDirectory` instance.
|
|
26
|
-
|
|
27
|
-
```js
|
|
28
|
-
const { FlatDirectory } = require("gethstorage-sdk");
|
|
29
|
-
|
|
30
|
-
const rpc = "https://rpc.beta.testnet.l2.quarkchain.io:8545";
|
|
31
|
-
const ethStorageRpc = "https://rpc.beta.testnet.l2.ethstorage.io:9596";
|
|
32
|
-
const privateKey = "0xabcd...";
|
|
33
|
-
|
|
34
|
-
const flatDirectory = await FlatDirectory.create({
|
|
35
|
-
rpc: rpc,
|
|
36
|
-
ethStorageRpc: ethStorageRpc,
|
|
37
|
-
privateKey: privateKey,
|
|
38
|
-
});
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
If `FlatDirectory` has been deployed, it can be set through the `address` field.
|
|
42
|
-
|
|
43
|
-
```js
|
|
44
|
-
const address = "0x987..."; // FlatDirectory address
|
|
45
|
-
const flatDirectory = await FlatDirectory.create({
|
|
46
|
-
rpc: rpc,
|
|
47
|
-
ethStorageRpc: ethStorageRpc,
|
|
48
|
-
privateKey: privateKey,
|
|
49
|
-
address: address,
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
If you only need the `download` function, you can create a `FlatDirectory` using the following pattern.
|
|
54
|
-
|
|
55
|
-
```js
|
|
56
|
-
const address = "0x987..."; // FlatDirectory address
|
|
57
|
-
const flatDirectory = await FlatDirectory.create({
|
|
58
|
-
ethStorageRpc: 'https://rpc.beta.testnet.l2.ethstorage.io:9596',
|
|
59
|
-
address: address
|
|
60
|
-
});
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### deploy
|
|
64
|
-
|
|
65
|
-
Deploy the implementation
|
|
66
|
-
contract [FlatDirectory](https://github.com/ethstorage/evm-large-storage)
|
|
67
|
-
for [EIP-5018](https://eips.ethereum.org/EIPS/eip-5018) standard.
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
const contractAddress = await flatDirectory.deploy();
|
|
71
|
-
console.log(`FlatDirectory address is ${contractAddress}.`);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### upload
|
|
75
|
-
|
|
76
|
-
Upload `buffer | file` to the FlatDirectory. The content is sliced into chunks and each chunk is sent through calldata.
|
|
77
|
-
|
|
78
|
-
```js
|
|
79
|
-
const callback = {
|
|
80
|
-
onProgress: function (progress, count, isChange) {
|
|
81
|
-
console.log(`Uploaded ${progress} of ${count} chunks`);
|
|
82
|
-
},
|
|
83
|
-
onFail: function (err) {
|
|
84
|
-
console.log(err);
|
|
85
|
-
},
|
|
86
|
-
onFinish: function (totalUploadChunks, totalUploadSize, totalCost) {
|
|
87
|
-
console.log(`Total upload chunk count is ${totalUploadChunks}, size is ${totalUploadSize}, total cost is ${totalCost}`);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const request = {
|
|
92
|
-
key: "test.txt",
|
|
93
|
-
content: Buffer.from("big data"),
|
|
94
|
-
callback: callback
|
|
95
|
-
}
|
|
96
|
-
await flatDirectory.upload(request);
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
If you want to use `file`, it can be divided into browser and Node.js.
|
|
100
|
-
|
|
101
|
-
Browser
|
|
102
|
-
```javascript
|
|
103
|
-
// <input id='fileToUpload' />
|
|
104
|
-
const file = document.getElementById('fileToUpload').files[0];
|
|
105
|
-
|
|
106
|
-
const request = {
|
|
107
|
-
key: "test.txt",
|
|
108
|
-
content: file,
|
|
109
|
-
callback: callback
|
|
110
|
-
}
|
|
111
|
-
await flatDirectory.upload(request);
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Node.js
|
|
115
|
-
```javascript
|
|
116
|
-
const {NodeFile} = require("gethstorage-sdk/file");
|
|
117
|
-
const file = new NodeFile("/usr/download/test.jpg");
|
|
118
|
-
|
|
119
|
-
const request = {
|
|
120
|
-
key: "test.txt",
|
|
121
|
-
content: file,
|
|
122
|
-
callback: callback
|
|
123
|
-
}
|
|
124
|
-
await flatDirectory.upload(request);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### download
|
|
128
|
-
|
|
129
|
-
Monitor the download progress by passing in a callback function.
|
|
130
|
-
|
|
131
|
-
```js
|
|
132
|
-
const key = "test.txt";
|
|
133
|
-
await flatDirectory.download(key, {
|
|
134
|
-
onProgress: function (progress, count, chunk) {
|
|
135
|
-
console.log(`Download ${progress} of ${count} chunks, this chunk is ${Buffer.from(chunk).toString()}`);
|
|
136
|
-
},
|
|
137
|
-
onFail: function (error) {
|
|
138
|
-
console.error("Error download data:", error);
|
|
139
|
-
},
|
|
140
|
-
onFinish: function () {
|
|
141
|
-
console.log("Download success.");
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### estimateCost
|
|
147
|
-
|
|
148
|
-
Estimate gas costs before uploading.
|
|
149
|
-
|
|
150
|
-
```js
|
|
151
|
-
const request = {
|
|
152
|
-
key: "example1.txt",
|
|
153
|
-
content: Buffer.from("large data to upload")
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const cost = await flatDirectory.estimateCost(request);
|
|
157
|
-
console.log(`Gas Cost: ${cost.gasCost}, Storage Cost: ${cost.storageCost}`);
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Use `file`.
|
|
161
|
-
|
|
162
|
-
Browser
|
|
163
|
-
```javascript
|
|
164
|
-
// <input id='fileToUpload' />
|
|
165
|
-
const file = document.getElementById('fileToUpload').files[0];
|
|
166
|
-
|
|
167
|
-
const request = {
|
|
168
|
-
key: "example1.txt",
|
|
169
|
-
content: file
|
|
170
|
-
}
|
|
171
|
-
const cost = await flatDirectory.estimateCost(request);
|
|
172
|
-
console.log(`Gas Cost: ${cost.gasCost}, Storage Cost: ${cost.storageCost}`);
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Node.js
|
|
176
|
-
```javascript
|
|
177
|
-
const {NodeFile} = require("gethstorage-sdk/file");
|
|
178
|
-
const file = new NodeFile("/usr/download/test.jpg");
|
|
179
|
-
|
|
180
|
-
const request = {
|
|
181
|
-
key: "example1.txt",
|
|
182
|
-
content: file
|
|
183
|
-
}
|
|
184
|
-
const cost = await flatDirectory.estimateCost(request);
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
### Chunk options
|
|
189
|
-
|
|
190
|
-
Uploads are sliced into calldata chunks. Both `estimateCost` and `upload`
|
|
191
|
-
requests accept two optional fields:
|
|
192
|
-
|
|
193
|
-
| Field | Type | Default | Description |
|
|
194
|
-
|----------------|--------|---------|----------------------------------------------------------|
|
|
195
|
-
| `maxChunkSize` | number | `64` | data size of each chunk, in KB |
|
|
196
|
-
| `maxChunks` | number | `100` | maximum number of chunks requested in a single call |
|
|
197
|
-
|
|
198
|
-
```js
|
|
199
|
-
const request = {
|
|
200
|
-
key: "example1.txt",
|
|
201
|
-
content: Buffer.from("large data to upload"),
|
|
202
|
-
maxChunkSize: 32, // 32 KB per chunk
|
|
203
|
-
maxChunks: 120 // at most 120 chunks per request
|
|
204
|
-
}
|
|
205
|
-
await flatDirectory.upload({ ...request, callback });
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`fetchHashes(keys, maxChunks)` accepts the same chunk limit as its second argument.
|
|
1
|
+
# gethstorage-sdk
|
|
2
|
+
|
|
3
|
+
This SDK aims to standardize the interaction between applications and the EthStorage network to achieve reliable and
|
|
4
|
+
efficient data management functionality.
|
|
5
|
+
|
|
6
|
+
This SDK provides the `FlatDirectory` class, a higher-level data management tool that provides methods for uploading
|
|
7
|
+
and downloading data of arbitrary size. Uploads are performed by sending the data through calldata.
|
|
8
|
+
|
|
9
|
+
Click here to view [spec](./spec.md).
|
|
10
|
+
|
|
11
|
+
# Installation
|
|
12
|
+
|
|
13
|
+
Install the SDK using [npm](https://www.npmjs.com/package/gethstorage-sdk):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ npm install gethstorage-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
# Example Usage
|
|
20
|
+
|
|
21
|
+
## FlatDirectory
|
|
22
|
+
|
|
23
|
+
### create
|
|
24
|
+
|
|
25
|
+
Create a `FlatDirectory` instance.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const { FlatDirectory } = require("gethstorage-sdk");
|
|
29
|
+
|
|
30
|
+
const rpc = "https://rpc.beta.testnet.l2.quarkchain.io:8545";
|
|
31
|
+
const ethStorageRpc = "https://rpc.beta.testnet.l2.ethstorage.io:9596";
|
|
32
|
+
const privateKey = "0xabcd...";
|
|
33
|
+
|
|
34
|
+
const flatDirectory = await FlatDirectory.create({
|
|
35
|
+
rpc: rpc,
|
|
36
|
+
ethStorageRpc: ethStorageRpc,
|
|
37
|
+
privateKey: privateKey,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If `FlatDirectory` has been deployed, it can be set through the `address` field.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const address = "0x987..."; // FlatDirectory address
|
|
45
|
+
const flatDirectory = await FlatDirectory.create({
|
|
46
|
+
rpc: rpc,
|
|
47
|
+
ethStorageRpc: ethStorageRpc,
|
|
48
|
+
privateKey: privateKey,
|
|
49
|
+
address: address,
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you only need the `download` function, you can create a `FlatDirectory` using the following pattern.
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const address = "0x987..."; // FlatDirectory address
|
|
57
|
+
const flatDirectory = await FlatDirectory.create({
|
|
58
|
+
ethStorageRpc: 'https://rpc.beta.testnet.l2.ethstorage.io:9596',
|
|
59
|
+
address: address
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### deploy
|
|
64
|
+
|
|
65
|
+
Deploy the implementation
|
|
66
|
+
contract [FlatDirectory](https://github.com/ethstorage/evm-large-storage)
|
|
67
|
+
for [EIP-5018](https://eips.ethereum.org/EIPS/eip-5018) standard.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const contractAddress = await flatDirectory.deploy();
|
|
71
|
+
console.log(`FlatDirectory address is ${contractAddress}.`);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### upload
|
|
75
|
+
|
|
76
|
+
Upload `buffer | file` to the FlatDirectory. The content is sliced into chunks and each chunk is sent through calldata.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const callback = {
|
|
80
|
+
onProgress: function (progress, count, isChange) {
|
|
81
|
+
console.log(`Uploaded ${progress} of ${count} chunks`);
|
|
82
|
+
},
|
|
83
|
+
onFail: function (err) {
|
|
84
|
+
console.log(err);
|
|
85
|
+
},
|
|
86
|
+
onFinish: function (totalUploadChunks, totalUploadSize, totalCost) {
|
|
87
|
+
console.log(`Total upload chunk count is ${totalUploadChunks}, size is ${totalUploadSize}, total cost is ${totalCost}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const request = {
|
|
92
|
+
key: "test.txt",
|
|
93
|
+
content: Buffer.from("big data"),
|
|
94
|
+
callback: callback
|
|
95
|
+
}
|
|
96
|
+
await flatDirectory.upload(request);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If you want to use `file`, it can be divided into browser and Node.js.
|
|
100
|
+
|
|
101
|
+
Browser
|
|
102
|
+
```javascript
|
|
103
|
+
// <input id='fileToUpload' />
|
|
104
|
+
const file = document.getElementById('fileToUpload').files[0];
|
|
105
|
+
|
|
106
|
+
const request = {
|
|
107
|
+
key: "test.txt",
|
|
108
|
+
content: file,
|
|
109
|
+
callback: callback
|
|
110
|
+
}
|
|
111
|
+
await flatDirectory.upload(request);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Node.js
|
|
115
|
+
```javascript
|
|
116
|
+
const {NodeFile} = require("gethstorage-sdk/file");
|
|
117
|
+
const file = new NodeFile("/usr/download/test.jpg");
|
|
118
|
+
|
|
119
|
+
const request = {
|
|
120
|
+
key: "test.txt",
|
|
121
|
+
content: file,
|
|
122
|
+
callback: callback
|
|
123
|
+
}
|
|
124
|
+
await flatDirectory.upload(request);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### download
|
|
128
|
+
|
|
129
|
+
Monitor the download progress by passing in a callback function.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const key = "test.txt";
|
|
133
|
+
await flatDirectory.download(key, {
|
|
134
|
+
onProgress: function (progress, count, chunk) {
|
|
135
|
+
console.log(`Download ${progress} of ${count} chunks, this chunk is ${Buffer.from(chunk).toString()}`);
|
|
136
|
+
},
|
|
137
|
+
onFail: function (error) {
|
|
138
|
+
console.error("Error download data:", error);
|
|
139
|
+
},
|
|
140
|
+
onFinish: function () {
|
|
141
|
+
console.log("Download success.");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### estimateCost
|
|
147
|
+
|
|
148
|
+
Estimate gas costs before uploading.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
const request = {
|
|
152
|
+
key: "example1.txt",
|
|
153
|
+
content: Buffer.from("large data to upload")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cost = await flatDirectory.estimateCost(request);
|
|
157
|
+
console.log(`Gas Cost: ${cost.gasCost}, Storage Cost: ${cost.storageCost}`);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use `file`.
|
|
161
|
+
|
|
162
|
+
Browser
|
|
163
|
+
```javascript
|
|
164
|
+
// <input id='fileToUpload' />
|
|
165
|
+
const file = document.getElementById('fileToUpload').files[0];
|
|
166
|
+
|
|
167
|
+
const request = {
|
|
168
|
+
key: "example1.txt",
|
|
169
|
+
content: file
|
|
170
|
+
}
|
|
171
|
+
const cost = await flatDirectory.estimateCost(request);
|
|
172
|
+
console.log(`Gas Cost: ${cost.gasCost}, Storage Cost: ${cost.storageCost}`);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Node.js
|
|
176
|
+
```javascript
|
|
177
|
+
const {NodeFile} = require("gethstorage-sdk/file");
|
|
178
|
+
const file = new NodeFile("/usr/download/test.jpg");
|
|
179
|
+
|
|
180
|
+
const request = {
|
|
181
|
+
key: "example1.txt",
|
|
182
|
+
content: file
|
|
183
|
+
}
|
|
184
|
+
const cost = await flatDirectory.estimateCost(request);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### Chunk options
|
|
189
|
+
|
|
190
|
+
Uploads are sliced into calldata chunks. Both `estimateCost` and `upload`
|
|
191
|
+
requests accept two optional fields:
|
|
192
|
+
|
|
193
|
+
| Field | Type | Default | Description |
|
|
194
|
+
|----------------|--------|---------|----------------------------------------------------------|
|
|
195
|
+
| `maxChunkSize` | number | `64` | data size of each chunk, in KB |
|
|
196
|
+
| `maxChunks` | number | `100` | maximum number of chunks requested in a single call |
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const request = {
|
|
200
|
+
key: "example1.txt",
|
|
201
|
+
content: Buffer.from("large data to upload"),
|
|
202
|
+
maxChunkSize: 32, // 32 KB per chunk
|
|
203
|
+
maxChunks: 120 // at most 120 chunks per request
|
|
204
|
+
}
|
|
205
|
+
await flatDirectory.upload({ ...request, callback });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`fetchHashes(keys, maxChunks)` accepts the same chunk limit as its second argument.
|
package/dist/browser/index.d.mts
CHANGED
|
@@ -162,8 +162,44 @@ interface ChunkHashResult {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
declare const stringToHex: (s: string) => string;
|
|
165
|
+
/**
|
|
166
|
+
* EIP-3541 forbids deploying new contract code whose first byte is 0xEF.
|
|
167
|
+
* EthStorage stores each chunk as a contract via CREATE2, so a chunk whose
|
|
168
|
+
* first byte is 0xEF will revert. We avoid this by shifting chunk boundaries.
|
|
169
|
+
*/
|
|
170
|
+
declare const EF_MAGIC: number;
|
|
171
|
+
/**
|
|
172
|
+
* Number of bytes reserved at the end of each chunk as extension headroom.
|
|
173
|
+
*
|
|
174
|
+
* The base chunk size is reduced by this many bytes; when a boundary would
|
|
175
|
+
* leave the next chunk starting with 0xEF, the current chunk is extended by
|
|
176
|
+
* up to this many bytes to absorb the run. The extended size never exceeds
|
|
177
|
+
* the original (unreserved) chunk size, so the contract's maxChunkSize limit
|
|
178
|
+
* is never violated.
|
|
179
|
+
*
|
|
180
|
+
* Probability justification (uniform random bytes):
|
|
181
|
+
* P(16 consecutive 0xEF) = (1/256)^16 ≈ 1e-38, effectively zero.
|
|
182
|
+
* Even across billions of boundaries the probability remains negligible.
|
|
183
|
+
* If a longer run ever occurs, the shrink fallback handles it.
|
|
184
|
+
*/
|
|
185
|
+
declare const EF_RESERVED_BYTES: number;
|
|
186
|
+
interface ChunkBoundary {
|
|
187
|
+
start: number;
|
|
188
|
+
end: number;
|
|
189
|
+
}
|
|
165
190
|
declare function getChainId(rpc: string): Promise<number>;
|
|
166
191
|
declare function getContentChunk(content: ContentLike, start: number, end: number): Promise<Uint8Array<ArrayBuffer>>;
|
|
192
|
+
/**
|
|
193
|
+
* Compute chunk boundaries for a calldata upload, shifting any boundary that
|
|
194
|
+
* would leave the next chunk starting with the 0xEF magic byte (EIP-3541).
|
|
195
|
+
*
|
|
196
|
+
* Strategy: for each boundary, if the byte at the boundary (first byte of the
|
|
197
|
+
* next chunk) is 0xEF, extend the current chunk to absorb it. If extension
|
|
198
|
+
* would exceed maxChunkSize, shrink the current chunk instead. In the extremely
|
|
199
|
+
* unlikely case that every byte in the adjustment window is 0xEF, the original
|
|
200
|
+
* fixed boundary is kept.
|
|
201
|
+
*/
|
|
202
|
+
declare function computeCalldataChunkBoundaries(content: ContentLike, baseChunkSize: number, maxChunkSize: number): Promise<ChunkBoundary[]>;
|
|
167
203
|
declare function isBuffer(content: ContentLike): content is BufferLike;
|
|
168
204
|
declare function isFile(content: ContentLike): content is FileLike;
|
|
169
205
|
declare function isNodejs(): boolean;
|
|
@@ -212,9 +248,13 @@ declare function getChunkHashes(contract: Contract, batch: FileBatch[]): Promise
|
|
|
212
248
|
|
|
213
249
|
declare function stableRetry<T>(fn: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T>;
|
|
214
250
|
|
|
251
|
+
type index_ChunkBoundary = ChunkBoundary;
|
|
252
|
+
declare const index_EF_MAGIC: typeof EF_MAGIC;
|
|
253
|
+
declare const index_EF_RESERVED_BYTES: typeof EF_RESERVED_BYTES;
|
|
215
254
|
type index_TxSender = TxSender;
|
|
216
255
|
declare const index_TxSender: typeof TxSender;
|
|
217
256
|
declare const index_calcTxCost: typeof calcTxCost;
|
|
257
|
+
declare const index_computeCalldataChunkBoundaries: typeof computeCalldataChunkBoundaries;
|
|
218
258
|
declare const index_getChainId: typeof getChainId;
|
|
219
259
|
declare const index_getChunkCounts: typeof getChunkCounts;
|
|
220
260
|
declare const index_getChunkHashes: typeof getChunkHashes;
|
|
@@ -226,7 +266,7 @@ declare const index_isNodejs: typeof isNodejs;
|
|
|
226
266
|
declare const index_stableRetry: typeof stableRetry;
|
|
227
267
|
declare const index_stringToHex: typeof stringToHex;
|
|
228
268
|
declare namespace index {
|
|
229
|
-
export { index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
269
|
+
export { type index_ChunkBoundary as ChunkBoundary, index_EF_MAGIC as EF_MAGIC, index_EF_RESERVED_BYTES as EF_RESERVED_BYTES, index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_computeCalldataChunkBoundaries as computeCalldataChunkBoundaries, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
declare class FlatDirectory {
|
package/dist/browser/index.mjs
CHANGED
|
@@ -7,8 +7,11 @@ var __export = (target, all) => {
|
|
|
7
7
|
// src/utils/index.ts
|
|
8
8
|
var utils_exports = {};
|
|
9
9
|
__export(utils_exports, {
|
|
10
|
+
EF_MAGIC: () => EF_MAGIC,
|
|
11
|
+
EF_RESERVED_BYTES: () => EF_RESERVED_BYTES,
|
|
10
12
|
TxSender: () => TxSender,
|
|
11
13
|
calcTxCost: () => calcTxCost,
|
|
14
|
+
computeCalldataChunkBoundaries: () => computeCalldataChunkBoundaries,
|
|
12
15
|
getChainId: () => getChainId,
|
|
13
16
|
getChunkCounts: () => getChunkCounts,
|
|
14
17
|
getChunkHashes: () => getChunkHashes,
|
|
@@ -24,6 +27,8 @@ __export(utils_exports, {
|
|
|
24
27
|
// src/utils/util.ts
|
|
25
28
|
import { ethers } from "ethers";
|
|
26
29
|
var stringToHex = (s) => ethers.hexlify(ethers.toUtf8Bytes(s));
|
|
30
|
+
var EF_MAGIC = 239;
|
|
31
|
+
var EF_RESERVED_BYTES = 16;
|
|
27
32
|
async function getChainId(rpc) {
|
|
28
33
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
29
34
|
const network = await provider.getNetwork();
|
|
@@ -38,6 +43,50 @@ async function getContentChunk(content, start, end) {
|
|
|
38
43
|
return new Uint8Array(data);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
46
|
+
async function computeCalldataChunkBoundaries(content, baseChunkSize, maxChunkSize) {
|
|
47
|
+
const size = isFile(content) ? content.size : content.length;
|
|
48
|
+
const boundaries = [];
|
|
49
|
+
let pos = 0;
|
|
50
|
+
while (pos < size) {
|
|
51
|
+
let end = Math.min(pos + baseChunkSize, size);
|
|
52
|
+
if (end < size) {
|
|
53
|
+
const maxExtendEnd = Math.min(pos + maxChunkSize, size);
|
|
54
|
+
if (maxExtendEnd > end) {
|
|
55
|
+
const extWindow = await getContentChunk(content, end, maxExtendEnd);
|
|
56
|
+
let safeOffset = -1;
|
|
57
|
+
for (let i = 0; i < extWindow.length; i++) {
|
|
58
|
+
if (extWindow[i] !== EF_MAGIC) {
|
|
59
|
+
safeOffset = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (safeOffset >= 0) {
|
|
64
|
+
end = end + safeOffset;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (end < size) {
|
|
68
|
+
const firstByte = await getContentChunk(content, end, end + 1);
|
|
69
|
+
if (firstByte.length > 0 && firstByte[0] === EF_MAGIC) {
|
|
70
|
+
const shrinkStart = Math.max(pos, end - 256);
|
|
71
|
+
const shrinkWindow = await getContentChunk(content, shrinkStart, end);
|
|
72
|
+
let lastSafe = -1;
|
|
73
|
+
for (let i = shrinkWindow.length - 1; i >= 0; i--) {
|
|
74
|
+
if (shrinkWindow[i] !== EF_MAGIC) {
|
|
75
|
+
lastSafe = shrinkStart + i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (lastSafe > pos) {
|
|
80
|
+
end = lastSafe;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
boundaries.push({ start: pos, end });
|
|
86
|
+
pos = end;
|
|
87
|
+
}
|
|
88
|
+
return boundaries;
|
|
89
|
+
}
|
|
41
90
|
function isBuffer(content) {
|
|
42
91
|
return content instanceof Uint8Array;
|
|
43
92
|
}
|
|
@@ -762,10 +811,13 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
762
811
|
if (toChunkSizeBytes(maxChunkSize) > CONTRACT_MAX_CHUNK_SIZE) {
|
|
763
812
|
throw new Error(`FlatDirectory: Chunk size ${maxChunkSize} KB exceeds the on-chain max payload of ${CONTRACT_MAX_CHUNK_SIZE} bytes.`);
|
|
764
813
|
}
|
|
765
|
-
const { chunkDataSize
|
|
814
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
766
815
|
if (chunkDataSize === -1) {
|
|
767
816
|
throw new Error(`FlatDirectory: Invalid upload content!`);
|
|
768
817
|
}
|
|
818
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
819
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
820
|
+
const calldataChunkCount = boundaries.length;
|
|
769
821
|
const hexName = stringToHex(key);
|
|
770
822
|
const fileContract = new ethers3.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
771
823
|
const [{ oldChunkCount, fileMode }, resolvedFees] = await Promise.all([
|
|
@@ -791,7 +843,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
791
843
|
() => fileContract["writeChunkByCalldata"].estimateGas(hexName, chunkId, hexData)
|
|
792
844
|
);
|
|
793
845
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
794
|
-
const
|
|
846
|
+
const { start, end } = boundaries[i];
|
|
847
|
+
const chunk = await getContentChunk(content, start, end);
|
|
795
848
|
if (i < chunkHashes.length && ethers3.keccak256(chunk) === chunkHashes[i]) {
|
|
796
849
|
continue;
|
|
797
850
|
}
|
|
@@ -830,12 +883,15 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
830
883
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
831
884
|
return;
|
|
832
885
|
}
|
|
833
|
-
const { chunkDataSize
|
|
886
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
834
887
|
if (chunkDataSize === -1) {
|
|
835
888
|
callback.onFail(new Error(`FlatDirectory: Invalid upload content!`));
|
|
836
889
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
837
890
|
return;
|
|
838
891
|
}
|
|
892
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
893
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
894
|
+
const calldataChunkCount = boundaries.length;
|
|
839
895
|
const hexName = stringToHex(key);
|
|
840
896
|
const fileContract = new ethers3.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
841
897
|
const { oldChunkCount, fileMode } = await getUploadInfo(fileContract, hexName);
|
|
@@ -876,7 +932,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
876
932
|
}
|
|
877
933
|
}
|
|
878
934
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
879
|
-
const
|
|
935
|
+
const { start, end } = boundaries[i];
|
|
936
|
+
const chunk = await getContentChunk(content, start, end);
|
|
880
937
|
if (i < chunkHashes.length && ethers3.keccak256(chunk) === chunkHashes[i]) {
|
|
881
938
|
callback.onProgress(i, calldataChunkCount, false);
|
|
882
939
|
continue;
|
package/dist/node/index.cjs
CHANGED
|
@@ -52,8 +52,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
52
52
|
// src/utils/index.ts
|
|
53
53
|
var utils_exports = {};
|
|
54
54
|
__export(utils_exports, {
|
|
55
|
+
EF_MAGIC: () => EF_MAGIC,
|
|
56
|
+
EF_RESERVED_BYTES: () => EF_RESERVED_BYTES,
|
|
55
57
|
TxSender: () => TxSender,
|
|
56
58
|
calcTxCost: () => calcTxCost,
|
|
59
|
+
computeCalldataChunkBoundaries: () => computeCalldataChunkBoundaries,
|
|
57
60
|
getChainId: () => getChainId,
|
|
58
61
|
getChunkCounts: () => getChunkCounts,
|
|
59
62
|
getChunkHashes: () => getChunkHashes,
|
|
@@ -69,6 +72,8 @@ __export(utils_exports, {
|
|
|
69
72
|
// src/utils/util.ts
|
|
70
73
|
var import_ethers = require("ethers");
|
|
71
74
|
var stringToHex = (s) => import_ethers.ethers.hexlify(import_ethers.ethers.toUtf8Bytes(s));
|
|
75
|
+
var EF_MAGIC = 239;
|
|
76
|
+
var EF_RESERVED_BYTES = 16;
|
|
72
77
|
async function getChainId(rpc) {
|
|
73
78
|
const provider = new import_ethers.ethers.JsonRpcProvider(rpc);
|
|
74
79
|
const network = await provider.getNetwork();
|
|
@@ -83,6 +88,50 @@ async function getContentChunk(content, start, end) {
|
|
|
83
88
|
return new Uint8Array(data);
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
async function computeCalldataChunkBoundaries(content, baseChunkSize, maxChunkSize) {
|
|
92
|
+
const size = isFile(content) ? content.size : content.length;
|
|
93
|
+
const boundaries = [];
|
|
94
|
+
let pos = 0;
|
|
95
|
+
while (pos < size) {
|
|
96
|
+
let end = Math.min(pos + baseChunkSize, size);
|
|
97
|
+
if (end < size) {
|
|
98
|
+
const maxExtendEnd = Math.min(pos + maxChunkSize, size);
|
|
99
|
+
if (maxExtendEnd > end) {
|
|
100
|
+
const extWindow = await getContentChunk(content, end, maxExtendEnd);
|
|
101
|
+
let safeOffset = -1;
|
|
102
|
+
for (let i = 0; i < extWindow.length; i++) {
|
|
103
|
+
if (extWindow[i] !== EF_MAGIC) {
|
|
104
|
+
safeOffset = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (safeOffset >= 0) {
|
|
109
|
+
end = end + safeOffset;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (end < size) {
|
|
113
|
+
const firstByte = await getContentChunk(content, end, end + 1);
|
|
114
|
+
if (firstByte.length > 0 && firstByte[0] === EF_MAGIC) {
|
|
115
|
+
const shrinkStart = Math.max(pos, end - 256);
|
|
116
|
+
const shrinkWindow = await getContentChunk(content, shrinkStart, end);
|
|
117
|
+
let lastSafe = -1;
|
|
118
|
+
for (let i = shrinkWindow.length - 1; i >= 0; i--) {
|
|
119
|
+
if (shrinkWindow[i] !== EF_MAGIC) {
|
|
120
|
+
lastSafe = shrinkStart + i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (lastSafe > pos) {
|
|
125
|
+
end = lastSafe;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
boundaries.push({ start: pos, end });
|
|
131
|
+
pos = end;
|
|
132
|
+
}
|
|
133
|
+
return boundaries;
|
|
134
|
+
}
|
|
86
135
|
function isBuffer(content) {
|
|
87
136
|
return content instanceof Uint8Array;
|
|
88
137
|
}
|
|
@@ -807,10 +856,13 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
807
856
|
if (toChunkSizeBytes(maxChunkSize) > CONTRACT_MAX_CHUNK_SIZE) {
|
|
808
857
|
throw new Error(`FlatDirectory: Chunk size ${maxChunkSize} KB exceeds the on-chain max payload of ${CONTRACT_MAX_CHUNK_SIZE} bytes.`);
|
|
809
858
|
}
|
|
810
|
-
const { chunkDataSize
|
|
859
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
811
860
|
if (chunkDataSize === -1) {
|
|
812
861
|
throw new Error(`FlatDirectory: Invalid upload content!`);
|
|
813
862
|
}
|
|
863
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
864
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
865
|
+
const calldataChunkCount = boundaries.length;
|
|
814
866
|
const hexName = stringToHex(key);
|
|
815
867
|
const fileContract = new import_ethers3.ethers.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
816
868
|
const [{ oldChunkCount, fileMode }, resolvedFees] = await Promise.all([
|
|
@@ -836,7 +888,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
836
888
|
() => fileContract["writeChunkByCalldata"].estimateGas(hexName, chunkId, hexData)
|
|
837
889
|
);
|
|
838
890
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
839
|
-
const
|
|
891
|
+
const { start, end } = boundaries[i];
|
|
892
|
+
const chunk = await getContentChunk(content, start, end);
|
|
840
893
|
if (i < chunkHashes.length && import_ethers3.ethers.keccak256(chunk) === chunkHashes[i]) {
|
|
841
894
|
continue;
|
|
842
895
|
}
|
|
@@ -875,12 +928,15 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
875
928
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
876
929
|
return;
|
|
877
930
|
}
|
|
878
|
-
const { chunkDataSize
|
|
931
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
879
932
|
if (chunkDataSize === -1) {
|
|
880
933
|
callback.onFail(new Error(`FlatDirectory: Invalid upload content!`));
|
|
881
934
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
882
935
|
return;
|
|
883
936
|
}
|
|
937
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
938
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
939
|
+
const calldataChunkCount = boundaries.length;
|
|
884
940
|
const hexName = stringToHex(key);
|
|
885
941
|
const fileContract = new import_ethers3.ethers.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
886
942
|
const { oldChunkCount, fileMode } = await getUploadInfo(fileContract, hexName);
|
|
@@ -921,7 +977,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
921
977
|
}
|
|
922
978
|
}
|
|
923
979
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
924
|
-
const
|
|
980
|
+
const { start, end } = boundaries[i];
|
|
981
|
+
const chunk = await getContentChunk(content, start, end);
|
|
925
982
|
if (i < chunkHashes.length && import_ethers3.ethers.keccak256(chunk) === chunkHashes[i]) {
|
|
926
983
|
callback.onProgress(i, calldataChunkCount, false);
|
|
927
984
|
continue;
|
package/dist/node/index.d.mts
CHANGED
|
@@ -162,8 +162,44 @@ interface ChunkHashResult {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
declare const stringToHex: (s: string) => string;
|
|
165
|
+
/**
|
|
166
|
+
* EIP-3541 forbids deploying new contract code whose first byte is 0xEF.
|
|
167
|
+
* EthStorage stores each chunk as a contract via CREATE2, so a chunk whose
|
|
168
|
+
* first byte is 0xEF will revert. We avoid this by shifting chunk boundaries.
|
|
169
|
+
*/
|
|
170
|
+
declare const EF_MAGIC: number;
|
|
171
|
+
/**
|
|
172
|
+
* Number of bytes reserved at the end of each chunk as extension headroom.
|
|
173
|
+
*
|
|
174
|
+
* The base chunk size is reduced by this many bytes; when a boundary would
|
|
175
|
+
* leave the next chunk starting with 0xEF, the current chunk is extended by
|
|
176
|
+
* up to this many bytes to absorb the run. The extended size never exceeds
|
|
177
|
+
* the original (unreserved) chunk size, so the contract's maxChunkSize limit
|
|
178
|
+
* is never violated.
|
|
179
|
+
*
|
|
180
|
+
* Probability justification (uniform random bytes):
|
|
181
|
+
* P(16 consecutive 0xEF) = (1/256)^16 ≈ 1e-38, effectively zero.
|
|
182
|
+
* Even across billions of boundaries the probability remains negligible.
|
|
183
|
+
* If a longer run ever occurs, the shrink fallback handles it.
|
|
184
|
+
*/
|
|
185
|
+
declare const EF_RESERVED_BYTES: number;
|
|
186
|
+
interface ChunkBoundary {
|
|
187
|
+
start: number;
|
|
188
|
+
end: number;
|
|
189
|
+
}
|
|
165
190
|
declare function getChainId(rpc: string): Promise<number>;
|
|
166
191
|
declare function getContentChunk(content: ContentLike, start: number, end: number): Promise<Uint8Array<ArrayBuffer>>;
|
|
192
|
+
/**
|
|
193
|
+
* Compute chunk boundaries for a calldata upload, shifting any boundary that
|
|
194
|
+
* would leave the next chunk starting with the 0xEF magic byte (EIP-3541).
|
|
195
|
+
*
|
|
196
|
+
* Strategy: for each boundary, if the byte at the boundary (first byte of the
|
|
197
|
+
* next chunk) is 0xEF, extend the current chunk to absorb it. If extension
|
|
198
|
+
* would exceed maxChunkSize, shrink the current chunk instead. In the extremely
|
|
199
|
+
* unlikely case that every byte in the adjustment window is 0xEF, the original
|
|
200
|
+
* fixed boundary is kept.
|
|
201
|
+
*/
|
|
202
|
+
declare function computeCalldataChunkBoundaries(content: ContentLike, baseChunkSize: number, maxChunkSize: number): Promise<ChunkBoundary[]>;
|
|
167
203
|
declare function isBuffer(content: ContentLike): content is BufferLike;
|
|
168
204
|
declare function isFile(content: ContentLike): content is FileLike;
|
|
169
205
|
declare function isNodejs(): boolean;
|
|
@@ -212,9 +248,13 @@ declare function getChunkHashes(contract: Contract, batch: FileBatch[]): Promise
|
|
|
212
248
|
|
|
213
249
|
declare function stableRetry<T>(fn: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T>;
|
|
214
250
|
|
|
251
|
+
type index_ChunkBoundary = ChunkBoundary;
|
|
252
|
+
declare const index_EF_MAGIC: typeof EF_MAGIC;
|
|
253
|
+
declare const index_EF_RESERVED_BYTES: typeof EF_RESERVED_BYTES;
|
|
215
254
|
type index_TxSender = TxSender;
|
|
216
255
|
declare const index_TxSender: typeof TxSender;
|
|
217
256
|
declare const index_calcTxCost: typeof calcTxCost;
|
|
257
|
+
declare const index_computeCalldataChunkBoundaries: typeof computeCalldataChunkBoundaries;
|
|
218
258
|
declare const index_getChainId: typeof getChainId;
|
|
219
259
|
declare const index_getChunkCounts: typeof getChunkCounts;
|
|
220
260
|
declare const index_getChunkHashes: typeof getChunkHashes;
|
|
@@ -226,7 +266,7 @@ declare const index_isNodejs: typeof isNodejs;
|
|
|
226
266
|
declare const index_stableRetry: typeof stableRetry;
|
|
227
267
|
declare const index_stringToHex: typeof stringToHex;
|
|
228
268
|
declare namespace index {
|
|
229
|
-
export { index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
269
|
+
export { type index_ChunkBoundary as ChunkBoundary, index_EF_MAGIC as EF_MAGIC, index_EF_RESERVED_BYTES as EF_RESERVED_BYTES, index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_computeCalldataChunkBoundaries as computeCalldataChunkBoundaries, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
declare class FlatDirectory {
|
package/dist/node/index.d.ts
CHANGED
|
@@ -162,8 +162,44 @@ interface ChunkHashResult {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
declare const stringToHex: (s: string) => string;
|
|
165
|
+
/**
|
|
166
|
+
* EIP-3541 forbids deploying new contract code whose first byte is 0xEF.
|
|
167
|
+
* EthStorage stores each chunk as a contract via CREATE2, so a chunk whose
|
|
168
|
+
* first byte is 0xEF will revert. We avoid this by shifting chunk boundaries.
|
|
169
|
+
*/
|
|
170
|
+
declare const EF_MAGIC: number;
|
|
171
|
+
/**
|
|
172
|
+
* Number of bytes reserved at the end of each chunk as extension headroom.
|
|
173
|
+
*
|
|
174
|
+
* The base chunk size is reduced by this many bytes; when a boundary would
|
|
175
|
+
* leave the next chunk starting with 0xEF, the current chunk is extended by
|
|
176
|
+
* up to this many bytes to absorb the run. The extended size never exceeds
|
|
177
|
+
* the original (unreserved) chunk size, so the contract's maxChunkSize limit
|
|
178
|
+
* is never violated.
|
|
179
|
+
*
|
|
180
|
+
* Probability justification (uniform random bytes):
|
|
181
|
+
* P(16 consecutive 0xEF) = (1/256)^16 ≈ 1e-38, effectively zero.
|
|
182
|
+
* Even across billions of boundaries the probability remains negligible.
|
|
183
|
+
* If a longer run ever occurs, the shrink fallback handles it.
|
|
184
|
+
*/
|
|
185
|
+
declare const EF_RESERVED_BYTES: number;
|
|
186
|
+
interface ChunkBoundary {
|
|
187
|
+
start: number;
|
|
188
|
+
end: number;
|
|
189
|
+
}
|
|
165
190
|
declare function getChainId(rpc: string): Promise<number>;
|
|
166
191
|
declare function getContentChunk(content: ContentLike, start: number, end: number): Promise<Uint8Array<ArrayBuffer>>;
|
|
192
|
+
/**
|
|
193
|
+
* Compute chunk boundaries for a calldata upload, shifting any boundary that
|
|
194
|
+
* would leave the next chunk starting with the 0xEF magic byte (EIP-3541).
|
|
195
|
+
*
|
|
196
|
+
* Strategy: for each boundary, if the byte at the boundary (first byte of the
|
|
197
|
+
* next chunk) is 0xEF, extend the current chunk to absorb it. If extension
|
|
198
|
+
* would exceed maxChunkSize, shrink the current chunk instead. In the extremely
|
|
199
|
+
* unlikely case that every byte in the adjustment window is 0xEF, the original
|
|
200
|
+
* fixed boundary is kept.
|
|
201
|
+
*/
|
|
202
|
+
declare function computeCalldataChunkBoundaries(content: ContentLike, baseChunkSize: number, maxChunkSize: number): Promise<ChunkBoundary[]>;
|
|
167
203
|
declare function isBuffer(content: ContentLike): content is BufferLike;
|
|
168
204
|
declare function isFile(content: ContentLike): content is FileLike;
|
|
169
205
|
declare function isNodejs(): boolean;
|
|
@@ -212,9 +248,13 @@ declare function getChunkHashes(contract: Contract, batch: FileBatch[]): Promise
|
|
|
212
248
|
|
|
213
249
|
declare function stableRetry<T>(fn: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T>;
|
|
214
250
|
|
|
251
|
+
type index_ChunkBoundary = ChunkBoundary;
|
|
252
|
+
declare const index_EF_MAGIC: typeof EF_MAGIC;
|
|
253
|
+
declare const index_EF_RESERVED_BYTES: typeof EF_RESERVED_BYTES;
|
|
215
254
|
type index_TxSender = TxSender;
|
|
216
255
|
declare const index_TxSender: typeof TxSender;
|
|
217
256
|
declare const index_calcTxCost: typeof calcTxCost;
|
|
257
|
+
declare const index_computeCalldataChunkBoundaries: typeof computeCalldataChunkBoundaries;
|
|
218
258
|
declare const index_getChainId: typeof getChainId;
|
|
219
259
|
declare const index_getChunkCounts: typeof getChunkCounts;
|
|
220
260
|
declare const index_getChunkHashes: typeof getChunkHashes;
|
|
@@ -226,7 +266,7 @@ declare const index_isNodejs: typeof isNodejs;
|
|
|
226
266
|
declare const index_stableRetry: typeof stableRetry;
|
|
227
267
|
declare const index_stringToHex: typeof stringToHex;
|
|
228
268
|
declare namespace index {
|
|
229
|
-
export { index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
269
|
+
export { type index_ChunkBoundary as ChunkBoundary, index_EF_MAGIC as EF_MAGIC, index_EF_RESERVED_BYTES as EF_RESERVED_BYTES, index_TxSender as TxSender, index_calcTxCost as calcTxCost, index_computeCalldataChunkBoundaries as computeCalldataChunkBoundaries, index_getChainId as getChainId, index_getChunkCounts as getChunkCounts, index_getChunkHashes as getChunkHashes, index_getContentChunk as getContentChunk, index_getUploadInfo as getUploadInfo, index_isBuffer as isBuffer, index_isFile as isFile, index_isNodejs as isNodejs, index_stableRetry as stableRetry, index_stringToHex as stringToHex };
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
declare class FlatDirectory {
|
package/dist/node/index.mjs
CHANGED
|
@@ -7,8 +7,11 @@ var __export = (target, all) => {
|
|
|
7
7
|
// src/utils/index.ts
|
|
8
8
|
var utils_exports = {};
|
|
9
9
|
__export(utils_exports, {
|
|
10
|
+
EF_MAGIC: () => EF_MAGIC,
|
|
11
|
+
EF_RESERVED_BYTES: () => EF_RESERVED_BYTES,
|
|
10
12
|
TxSender: () => TxSender,
|
|
11
13
|
calcTxCost: () => calcTxCost,
|
|
14
|
+
computeCalldataChunkBoundaries: () => computeCalldataChunkBoundaries,
|
|
12
15
|
getChainId: () => getChainId,
|
|
13
16
|
getChunkCounts: () => getChunkCounts,
|
|
14
17
|
getChunkHashes: () => getChunkHashes,
|
|
@@ -24,6 +27,8 @@ __export(utils_exports, {
|
|
|
24
27
|
// src/utils/util.ts
|
|
25
28
|
import { ethers } from "ethers";
|
|
26
29
|
var stringToHex = (s) => ethers.hexlify(ethers.toUtf8Bytes(s));
|
|
30
|
+
var EF_MAGIC = 239;
|
|
31
|
+
var EF_RESERVED_BYTES = 16;
|
|
27
32
|
async function getChainId(rpc) {
|
|
28
33
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
29
34
|
const network = await provider.getNetwork();
|
|
@@ -38,6 +43,50 @@ async function getContentChunk(content, start, end) {
|
|
|
38
43
|
return new Uint8Array(data);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
46
|
+
async function computeCalldataChunkBoundaries(content, baseChunkSize, maxChunkSize) {
|
|
47
|
+
const size = isFile(content) ? content.size : content.length;
|
|
48
|
+
const boundaries = [];
|
|
49
|
+
let pos = 0;
|
|
50
|
+
while (pos < size) {
|
|
51
|
+
let end = Math.min(pos + baseChunkSize, size);
|
|
52
|
+
if (end < size) {
|
|
53
|
+
const maxExtendEnd = Math.min(pos + maxChunkSize, size);
|
|
54
|
+
if (maxExtendEnd > end) {
|
|
55
|
+
const extWindow = await getContentChunk(content, end, maxExtendEnd);
|
|
56
|
+
let safeOffset = -1;
|
|
57
|
+
for (let i = 0; i < extWindow.length; i++) {
|
|
58
|
+
if (extWindow[i] !== EF_MAGIC) {
|
|
59
|
+
safeOffset = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (safeOffset >= 0) {
|
|
64
|
+
end = end + safeOffset;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (end < size) {
|
|
68
|
+
const firstByte = await getContentChunk(content, end, end + 1);
|
|
69
|
+
if (firstByte.length > 0 && firstByte[0] === EF_MAGIC) {
|
|
70
|
+
const shrinkStart = Math.max(pos, end - 256);
|
|
71
|
+
const shrinkWindow = await getContentChunk(content, shrinkStart, end);
|
|
72
|
+
let lastSafe = -1;
|
|
73
|
+
for (let i = shrinkWindow.length - 1; i >= 0; i--) {
|
|
74
|
+
if (shrinkWindow[i] !== EF_MAGIC) {
|
|
75
|
+
lastSafe = shrinkStart + i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (lastSafe > pos) {
|
|
80
|
+
end = lastSafe;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
boundaries.push({ start: pos, end });
|
|
86
|
+
pos = end;
|
|
87
|
+
}
|
|
88
|
+
return boundaries;
|
|
89
|
+
}
|
|
41
90
|
function isBuffer(content) {
|
|
42
91
|
return content instanceof Uint8Array;
|
|
43
92
|
}
|
|
@@ -762,10 +811,13 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
762
811
|
if (toChunkSizeBytes(maxChunkSize) > CONTRACT_MAX_CHUNK_SIZE) {
|
|
763
812
|
throw new Error(`FlatDirectory: Chunk size ${maxChunkSize} KB exceeds the on-chain max payload of ${CONTRACT_MAX_CHUNK_SIZE} bytes.`);
|
|
764
813
|
}
|
|
765
|
-
const { chunkDataSize
|
|
814
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
766
815
|
if (chunkDataSize === -1) {
|
|
767
816
|
throw new Error(`FlatDirectory: Invalid upload content!`);
|
|
768
817
|
}
|
|
818
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
819
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
820
|
+
const calldataChunkCount = boundaries.length;
|
|
769
821
|
const hexName = stringToHex(key);
|
|
770
822
|
const fileContract = new ethers3.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
771
823
|
const [{ oldChunkCount, fileMode }, resolvedFees] = await Promise.all([
|
|
@@ -791,7 +843,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
791
843
|
() => fileContract["writeChunkByCalldata"].estimateGas(hexName, chunkId, hexData)
|
|
792
844
|
);
|
|
793
845
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
794
|
-
const
|
|
846
|
+
const { start, end } = boundaries[i];
|
|
847
|
+
const chunk = await getContentChunk(content, start, end);
|
|
795
848
|
if (i < chunkHashes.length && ethers3.keccak256(chunk) === chunkHashes[i]) {
|
|
796
849
|
continue;
|
|
797
850
|
}
|
|
@@ -830,12 +883,15 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
830
883
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
831
884
|
return;
|
|
832
885
|
}
|
|
833
|
-
const { chunkDataSize
|
|
886
|
+
const { chunkDataSize } = this.#calculateCalldataChunkDetails(content, maxChunkSize);
|
|
834
887
|
if (chunkDataSize === -1) {
|
|
835
888
|
callback.onFail(new Error(`FlatDirectory: Invalid upload content!`));
|
|
836
889
|
callback.onFinish(totalUploadChunks, totalUploadSize, totalCost);
|
|
837
890
|
return;
|
|
838
891
|
}
|
|
892
|
+
const baseChunkSize = Math.max(1, chunkDataSize - EF_RESERVED_BYTES);
|
|
893
|
+
const boundaries = await computeCalldataChunkBoundaries(content, baseChunkSize, chunkDataSize);
|
|
894
|
+
const calldataChunkCount = boundaries.length;
|
|
839
895
|
const hexName = stringToHex(key);
|
|
840
896
|
const fileContract = new ethers3.Contract(this.#contractAddrChecked, FlatDirectoryAbi, this.#walletChecked);
|
|
841
897
|
const { oldChunkCount, fileMode } = await getUploadInfo(fileContract, hexName);
|
|
@@ -876,7 +932,8 @@ var FlatDirectory = class _FlatDirectory {
|
|
|
876
932
|
}
|
|
877
933
|
}
|
|
878
934
|
for (let i = 0; i < calldataChunkCount; i++) {
|
|
879
|
-
const
|
|
935
|
+
const { start, end } = boundaries[i];
|
|
936
|
+
const chunk = await getContentChunk(content, start, end);
|
|
880
937
|
if (i < chunkHashes.length && ethers3.keccak256(chunk) === chunkHashes[i]) {
|
|
881
938
|
callback.onProgress(i, calldataChunkCount, false);
|
|
882
939
|
continue;
|
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gethstorage-sdk",
|
|
3
|
-
"version": "5.0.
|
|
4
|
-
"description": "EthStorage flat directory upload sdk (calldata only)",
|
|
5
|
-
"files": [
|
|
6
|
-
"dist"
|
|
7
|
-
],
|
|
8
|
-
"main": "./dist/node/index.cjs",
|
|
9
|
-
"types": "./dist/node/index.d.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"browser": "./dist/browser/index.mjs",
|
|
13
|
-
"node": {
|
|
14
|
-
"import": "./dist/node/index.mjs",
|
|
15
|
-
"require": "./dist/node/index.cjs"
|
|
16
|
-
},
|
|
17
|
-
"types": "./dist/node/index.d.ts",
|
|
18
|
-
"default": "./dist/node/index.mjs"
|
|
19
|
-
},
|
|
20
|
-
"./file": {
|
|
21
|
-
"node": {
|
|
22
|
-
"import": "./dist/node/file.mjs",
|
|
23
|
-
"require": "./dist/node/file.cjs"
|
|
24
|
-
},
|
|
25
|
-
"types": "./dist/node/file.d.ts"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
30
|
-
"dev": "tsup --watch --env.NODE_ENV development",
|
|
31
|
-
"prepublishOnly": "npm run build"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"dotenv": "^16.4.5",
|
|
35
|
-
"ethers": "^6.17.0",
|
|
36
|
-
"p-limit": "^7.2.0"
|
|
37
|
-
},
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/0xCampanula/gethstorage-sdk.git"
|
|
41
|
-
},
|
|
42
|
-
"bugs": {
|
|
43
|
-
"url": "https://github.com/0xCampanula/gethstorage-sdk/issues"
|
|
44
|
-
},
|
|
45
|
-
"homepage": "https://github.com/0xCampanula/gethstorage-sdk/main/",
|
|
46
|
-
"keywords": [
|
|
47
|
-
"File",
|
|
48
|
-
"Upload"
|
|
49
|
-
],
|
|
50
|
-
"author": "ethsorage",
|
|
51
|
-
"license": "ISC",
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"tsup": "^8.5.1",
|
|
54
|
-
"typescript": "^5.9.3"
|
|
55
|
-
},
|
|
56
|
-
"resolutions": {
|
|
57
|
-
"strip-ansi": "7.1.0",
|
|
58
|
-
"color-convert": "2.0.1",
|
|
59
|
-
"color-name": "1.1.4"
|
|
60
|
-
},
|
|
61
|
-
"overrides": {
|
|
62
|
-
"rollup": ">=4.59.0",
|
|
63
|
-
"picomatch": ">=4.0.4"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gethstorage-sdk",
|
|
3
|
+
"version": "5.0.3",
|
|
4
|
+
"description": "EthStorage flat directory upload sdk (calldata only)",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/node/index.cjs",
|
|
9
|
+
"types": "./dist/node/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"browser": "./dist/browser/index.mjs",
|
|
13
|
+
"node": {
|
|
14
|
+
"import": "./dist/node/index.mjs",
|
|
15
|
+
"require": "./dist/node/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/node/index.d.ts",
|
|
18
|
+
"default": "./dist/node/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./file": {
|
|
21
|
+
"node": {
|
|
22
|
+
"import": "./dist/node/file.mjs",
|
|
23
|
+
"require": "./dist/node/file.cjs"
|
|
24
|
+
},
|
|
25
|
+
"types": "./dist/node/file.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
30
|
+
"dev": "tsup --watch --env.NODE_ENV development",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"dotenv": "^16.4.5",
|
|
35
|
+
"ethers": "^6.17.0",
|
|
36
|
+
"p-limit": "^7.2.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/0xCampanula/gethstorage-sdk.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/0xCampanula/gethstorage-sdk/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/0xCampanula/gethstorage-sdk/main/",
|
|
46
|
+
"keywords": [
|
|
47
|
+
"File",
|
|
48
|
+
"Upload"
|
|
49
|
+
],
|
|
50
|
+
"author": "ethsorage",
|
|
51
|
+
"license": "ISC",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"tsup": "^8.5.1",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"resolutions": {
|
|
57
|
+
"strip-ansi": "7.1.0",
|
|
58
|
+
"color-convert": "2.0.1",
|
|
59
|
+
"color-name": "1.1.4"
|
|
60
|
+
},
|
|
61
|
+
"overrides": {
|
|
62
|
+
"rollup": ">=4.59.0",
|
|
63
|
+
"picomatch": ">=4.0.4"
|
|
64
|
+
}
|
|
65
|
+
}
|