@nosense00/seal
Version:
Seal project for Tea.xyz OSS registry
121 lines (112 loc) • 4.51 kB
text/typescript
import { SealClient, SessionKey, NoAccessError, EncryptedObject } from '@mysten/seal';
import { SuiClient } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
import React from 'react';
export type MoveCallConstructor = (tx: Transaction, id: string) => void;
export const downloadAndDecrypt = async (
blobIds: string[],
sessionKey: SessionKey,
suiClient: SuiClient,
sealClient: SealClient,
moveCallConstructor: (tx: Transaction, id: string) => void,
setError: (error: string | null) => void,
setDecryptedFileUrls: (urls: string[]) => void,
setIsDialogOpen: (open: boolean) => void,
setReloadKey: (updater: (prev: number) => number) => void,
) => {
const aggregators = ['aggregator1', 'aggregator2', 'aggregator3', 'aggregator4', 'aggregator5', 'aggregator6'];
// First, download all files in parallel (ignore errors)
const downloadResults = await Promise.all(
blobIds.map(async (blobId) => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const randomAggregator = aggregators[Math.floor(Math.random() * aggregators.length)];
const aggregatorUrl = `/${randomAggregator}/v1/blobs/${blobId}`;
const response = await fetch(aggregatorUrl, { signal: controller.signal });
clearTimeout(timeout);
if (!response.ok) {
return null;
}
return await response.arrayBuffer();
} catch (err) {
console.error(`Blob ${blobId} cannot be retrieved from Walrus`, err);
return null;
}
}),
);
// Filter out failed downloads
const validDownloads = downloadResults.filter((result): result is ArrayBuffer => result !== null);
console.log('validDownloads count', validDownloads.length);
if (validDownloads.length === 0) {
const errorMsg =
'Cannot retrieve files from this Walrus aggregator, try again (a randomly selected aggregator will be used). Files uploaded more than 1 epoch ago have been deleted from Walrus.';
console.error(errorMsg);
setError(errorMsg);
return;
}
// Fetch keys in batches of <=10
for (let i = 0; i < validDownloads.length; i += 10) {
const batch = validDownloads.slice(i, i + 10);
const ids = batch.map((enc) => EncryptedObject.parse(new Uint8Array(enc)).id);
const tx = new Transaction();
ids.forEach((id) => moveCallConstructor(tx, id));
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
try {
await sealClient.fetchKeys({ ids, txBytes, sessionKey, threshold: 2 });
} catch (err) {
console.log(err);
const errorMsg =
err instanceof NoAccessError
? 'No access to decryption keys'
: 'Unable to decrypt files, try again';
console.error(errorMsg, err);
setError(errorMsg);
return;
}
}
// Then, decrypt files sequentially
const decryptedFileUrls: string[] = [];
for (const encryptedData of validDownloads) {
const fullId = EncryptedObject.parse(new Uint8Array(encryptedData)).id;
const tx = new Transaction();
moveCallConstructor(tx, fullId);
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
try {
// Note that all keys are fetched above, so this only local decryption is done
const decryptedFile = await sealClient.decrypt({
data: new Uint8Array(encryptedData),
sessionKey,
txBytes,
});
const blob = new Blob([decryptedFile], { type: 'image/jpg' });
decryptedFileUrls.push(URL.createObjectURL(blob));
} catch (err) {
console.log(err);
const errorMsg =
err instanceof NoAccessError
? 'No access to decryption keys'
: 'Unable to decrypt files, try again';
console.error(errorMsg, err);
setError(errorMsg);
return;
}
}
if (decryptedFileUrls.length > 0) {
setDecryptedFileUrls(decryptedFileUrls);
setIsDialogOpen(true);
setReloadKey((prev) => prev + 1);
}
};
export const getObjectExplorerLink = (id: string): React.ReactElement => {
return React.createElement(
'a',
{
href: `https://testnet.suivision.xyz/object/${id}`,
target: '_blank',
rel: 'noopener noreferrer',
style: { textDecoration: 'underline' },
},
id.slice(0, 10) + '...',
);
};