Skip to content

Commit 175231a

Browse files
feat(sdk): add arweave uploadMode option (#448)
1 parent af78bf8 commit 175231a

File tree

15 files changed

+1445
-55
lines changed

15 files changed

+1445
-55
lines changed

packages/sdk/package-lock.json

Lines changed: 866 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sdk/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"@swc/core": "^1.7.3",
7474
"@swc/jest": "^0.2.36",
7575
"@types/debug": "^4.1.7",
76+
"@types/express": "^5.0.2",
7677
"@types/jest": "^29.5.12",
7778
"@typescript-eslint/eslint-plugin": "^6.7.5",
7879
"@typescript-eslint/parser": "^6.7.5",
@@ -85,6 +86,7 @@
8586
"eslint-plugin-jest": "^27.4.2",
8687
"eslint-plugin-sonarjs": "^0.21.0",
8788
"eslint-plugin-unicorn": "^51.0.1",
89+
"express": "^5.1.0",
8890
"jest": "^29.7.0",
8991
"prettier": "^2.8.4",
9092
"rimraf": "^5.0.5",

packages/sdk/src/config/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export const CHAIN_CONFIG: Record<ChainId, ChainConfig> = {
2727
};
2828

2929
export const DEFAULT_CHAIN_ID = 134;
30+
export const DEFAULT_ARWEAVE_UPLOAD_API = 'https://arweave-api.iex.ec';
31+
export const DEFAULT_ARWEAVE_GATEWAY = 'https://arweave.net';
32+
export const ARWEAVE_FREE_UPLOAD_MAX_SIZE = 100 * 1024; // 100kb
3033
export const DEFAULT_DATA_NAME = '';
3134
export const SCONE_TAG = ['tee', 'scone'];
3235
export const DEFAULT_MAX_PRICE = 0;

packages/sdk/src/lib/IExecDataProtectorModule.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AbstractProvider, AbstractSigner, Eip1193Provider } from 'ethers';
22
import { GraphQLClient } from 'graphql-request';
33
import { IExec } from 'iexec';
4-
import { CHAIN_CONFIG } from '../config/config.js';
4+
import { CHAIN_CONFIG, DEFAULT_ARWEAVE_UPLOAD_API } from '../config/config.js';
55
import { getChainIdFromProvider } from '../utils/getChainId.js';
66
import {
77
AddressOrENS,
@@ -38,6 +38,8 @@ abstract class IExecDataProtectorModule {
3838

3939
protected ipfsGateway!: string;
4040

41+
protected arweaveUploadApi: string = DEFAULT_ARWEAVE_UPLOAD_API;
42+
4143
protected defaultWorkerpool!: string;
4244

4345
protected iexec!: IExec;

packages/sdk/src/lib/dataProtectorCore/IExecDataProtectorCore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class IExecDataProtectorCore extends IExecDataProtectorModule {
3939
dataprotectorContractAddress: this.dataprotectorContractAddress,
4040
ipfsNode: this.ipfsNode,
4141
ipfsGateway: this.ipfsGateway,
42+
arweaveUploadApi: this.arweaveUploadApi,
4243
iexec: this.iexec,
4344
iexecDebug: this.iexecDebug,
4445
});

packages/sdk/src/lib/dataProtectorCore/protectData.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { multiaddr as Multiaddr } from '@multiformats/multiaddr';
2-
import { DEFAULT_DATA_NAME } from '../../config/config.js';
3-
import { add } from '../../services/ipfs.js';
2+
import {
3+
DEFAULT_ARWEAVE_GATEWAY,
4+
DEFAULT_DATA_NAME,
5+
} from '../../config/config.js';
6+
import * as arweave from '../../services/arweave.js';
7+
import * as ipfs from '../../services/ipfs.js';
48
import {
59
createZipFromObject,
610
ensureDataObjectIsValid,
@@ -28,6 +32,7 @@ import {
2832
ProtectedDataWithSecretProps,
2933
} from '../types/index.js';
3034
import {
35+
ArweaveUploadConsumer,
3136
DataProtectorContractConsumer,
3237
IExecConsumer,
3338
IExecDebugConsumer,
@@ -43,17 +48,24 @@ export const protectData = async ({
4348
iexecDebug = throwIfMissing(),
4449
dataprotectorContractAddress,
4550
name = DEFAULT_DATA_NAME,
51+
uploadMode = 'ipfs',
4652
ipfsNode,
4753
ipfsGateway,
54+
arweaveUploadApi,
4855
allowDebug = false,
4956
data,
5057
onStatusUpdate = () => {},
5158
}: IExecConsumer &
5259
IExecDebugConsumer &
5360
DataProtectorContractConsumer &
5461
IpfsNodeAndGateway &
62+
ArweaveUploadConsumer &
5563
ProtectDataParams): Promise<ProtectedDataWithSecretProps> => {
5664
const vName = stringSchema().label('name').validateSync(name);
65+
const vUploadMode = stringSchema()
66+
.oneOf(['ipfs', 'arweave'])
67+
.label('uploadMode')
68+
.validateSync(uploadMode);
5769
const vIpfsNodeUrl = urlSchema().label('ipfsNode').validateSync(ipfsNode);
5870
const vIpfsGateway = urlSchema()
5971
.label('ipfsGateway')
@@ -149,28 +161,55 @@ export const protectData = async ({
149161
title: 'UPLOAD_ENCRYPTED_FILE',
150162
isDone: false,
151163
});
152-
const cid = await add(encryptedFile, {
153-
ipfsNode: vIpfsNodeUrl,
154-
ipfsGateway: vIpfsGateway,
155-
}).catch((e: Error) => {
156-
throw new WorkflowError({
157-
message: 'Failed to upload encrypted data',
158-
errorCause: e,
164+
165+
let multiaddr: string;
166+
let multiaddrBytes: Uint8Array;
167+
168+
if (vUploadMode === 'arweave') {
169+
const arweaveId = await arweave
170+
.add(encryptedFile, { arweaveUploadApi })
171+
.catch((e: Error) => {
172+
throw new WorkflowError({
173+
message: 'Failed to upload encrypted data',
174+
errorCause: e,
175+
});
176+
});
177+
multiaddr = `${DEFAULT_ARWEAVE_GATEWAY}/${arweaveId}`;
178+
multiaddrBytes = new TextEncoder().encode(multiaddr);
179+
vOnStatusUpdate({
180+
title: 'UPLOAD_ENCRYPTED_FILE',
181+
isDone: true,
182+
payload: {
183+
arweaveId,
184+
},
159185
});
160-
});
161-
const multiaddr = `/p2p/${cid}`;
162-
vOnStatusUpdate({
163-
title: 'UPLOAD_ENCRYPTED_FILE',
164-
isDone: true,
165-
payload: {
166-
cid,
167-
},
168-
});
186+
} else {
187+
// ipfs fallback
188+
const cid = await ipfs
189+
.add(encryptedFile, {
190+
ipfsNode: vIpfsNodeUrl,
191+
ipfsGateway: vIpfsGateway,
192+
})
193+
.catch((e: Error) => {
194+
throw new WorkflowError({
195+
message: 'Failed to upload encrypted data',
196+
errorCause: e,
197+
});
198+
});
199+
multiaddr = `/p2p/${cid}`;
200+
multiaddrBytes = Multiaddr(multiaddr).bytes;
201+
vOnStatusUpdate({
202+
title: 'UPLOAD_ENCRYPTED_FILE',
203+
isDone: true,
204+
payload: {
205+
cid,
206+
},
207+
});
208+
}
169209

170210
const { provider, signer, txOptions } =
171211
await iexec.config.resolveContractsClient();
172212

173-
const multiaddrBytes = Multiaddr(multiaddr).bytes;
174213
const ownerAddress = await signer.getAddress();
175214

176215
vOnStatusUpdate({

packages/sdk/src/lib/types/coreTypes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export type ProtectDataParams = {
7272
*/
7373
allowDebug?: boolean;
7474

75+
/**
76+
* specify the platform used for storing the encrypted payload of the protected data
77+
*
78+
* - `"ipfs"` (default): https://ipfs.tech/
79+
* - `"arweave"`: https://arweave.org/
80+
*/
81+
uploadMode?: 'ipfs' | 'arweave';
82+
7583
/**
7684
* Callback function that will get called at each step of the process
7785
*/
@@ -98,6 +106,7 @@ export type ProtectedDataCreationProps = {
98106
transactionHash: string;
99107
zipFile: Uint8Array;
100108
encryptionKey: string;
109+
multiaddr: string;
101110
};
102111

103112
export type ProtectedDataWithSecretProps = ProtectedData &

packages/sdk/src/lib/types/internalTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export type DataProtectorContractConsumer = {
1717
dataprotectorContractAddress: AddressOrENS;
1818
};
1919

20+
export type ArweaveUploadConsumer = {
21+
arweaveUploadApi?: string;
22+
};
23+
2024
export type SubgraphConsumer = {
2125
graphQLClient: GraphQLClient;
2226
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
DEFAULT_ARWEAVE_GATEWAY,
3+
DEFAULT_ARWEAVE_UPLOAD_API,
4+
ARWEAVE_FREE_UPLOAD_MAX_SIZE,
5+
} from '../config/config.js';
6+
7+
interface AddOptions {
8+
arweaveUploadApi?: string;
9+
arweaveGateway?: string;
10+
}
11+
12+
const add = async (
13+
content: Uint8Array,
14+
{ arweaveGateway, arweaveUploadApi }: AddOptions = {}
15+
): Promise<string> => {
16+
if (content.length >= ARWEAVE_FREE_UPLOAD_MAX_SIZE) {
17+
throw Error(
18+
`Arweave upload ${(ARWEAVE_FREE_UPLOAD_MAX_SIZE / 1024).toFixed(
19+
0
20+
)}kb size limit reached`
21+
);
22+
}
23+
24+
let arweaveId: string;
25+
try {
26+
const payload = new FormData();
27+
payload.append('file', new Blob([content]));
28+
const res = await fetch(
29+
`${arweaveUploadApi || DEFAULT_ARWEAVE_UPLOAD_API}/upload`,
30+
{
31+
method: 'POST',
32+
body: payload,
33+
}
34+
);
35+
if (!res.ok) {
36+
throw Error(`Arweave upload API answered with status ${res.status}`);
37+
}
38+
try {
39+
const data = await res.json();
40+
arweaveId = data?.arweaveId;
41+
if (!arweaveId) {
42+
throw Error('missing arweaveId');
43+
}
44+
} catch (e) {
45+
throw Error('Arweave upload API answered with an invalid response', {
46+
cause: e,
47+
});
48+
}
49+
} catch (e) {
50+
throw Error('Failed to add file on Arweave', { cause: e });
51+
}
52+
const publicUrl = `${arweaveGateway || DEFAULT_ARWEAVE_GATEWAY}/${arweaveId}`;
53+
await fetch(publicUrl)
54+
.then((res) => {
55+
if (!res.ok) {
56+
throw Error(`Arweave gateway answered with status ${res.status}`);
57+
}
58+
})
59+
.catch((e) => {
60+
throw Error(`Failed to load uploaded file at ${publicUrl}`, { cause: e });
61+
});
62+
63+
return arweaveId;
64+
};
65+
66+
export { add };

packages/sdk/src/utils/getMultiaddrAsString.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,28 @@ export function getMultiaddrAsString({
2222
if (!multiAddrAsBytesArray) {
2323
return undefined;
2424
}
25+
const multiaddrAsBytes = new Uint8Array(multiAddrAsBytesArray);
26+
27+
// try utf8 encoded url
28+
try {
29+
const decodedString = new TextDecoder().decode(multiaddrAsBytes);
30+
if (decodedString.startsWith('https://')) {
31+
return decodedString;
32+
}
33+
} catch {
34+
// noop
35+
}
36+
37+
// try machine format multiaddr
2538
try {
26-
const multiaddrAsBytes = new Uint8Array(multiAddrAsBytesArray);
2739
const decodedMultiaddr = multiaddr(multiaddrAsBytes);
2840
return decodedMultiaddr.toString();
29-
} catch (err) {
30-
console.warn(
31-
`[getMultiaddrAsString] ERROR: "${multiaddrAsHexString}" is not a valid hex input string?`,
32-
err
33-
);
34-
return undefined;
41+
} catch {
42+
// noop
3543
}
44+
45+
console.warn(
46+
`[getMultiaddrAsString] ERROR: "${multiaddrAsHexString}" is not a valid hex encoded multiaddr?`
47+
);
48+
return undefined;
3649
}

0 commit comments

Comments
 (0)