// import { Web3Storage } from 'web3.storage/dist/src/lib.d.ts' import type { NftStorageConnector } from '@wovin/connect-nftstorage' import type { CidString } from '@wovin/core/applog' // import * as W3Up from '@web3-storage/w3up-client' import type { WritableName } from 'w3name' import { UcanStoreProxyConnector } from '@wovin/connect-ucan-store-proxy' import { autorunButAlsoImmediately } from '@wovin/core' import { action, flow, toJS } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import { CID } from 'multiformats/cid' import { createStore } from 'solid-js/store' import * as W3Name from 'w3name' import { useAgent } from '../data/agent/AgentState' import { getVM } from '../data/VMs/MappedVMbase' import { ProviderVM } from '../data/VMs/ProviderVM' import { setStorageReachable, testStorageReachability } from '../ui/online-second' import { useCurrentThread, useProviderIDs, withDS } from '../ui/reactive' import { notifyToast } from '../ui/utils-ui' import { hasKubo, publishIpnsToKubo } from './kubo' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export const DEMO_PROVIDER = 'https://demo-storage.note3.app' export const parseW3Name = W3Name.parse // let w3up: W3Up.Client let name: WritableName const [storageState, setStorageState] = createStore({ nftStorage: null, ucanUploads: null, gateways: null, } as { nftStorage: NftStorageConnector | null ucanUploads: UcanStoreProxyConnector[] | null gateways: ProviderVM[] }) export { storageState } // @ts-expect-error window. if (typeof window != 'undefined') window.w3name = name export async function initStorage() { LOG('Initializing Storage...') const thread = useCurrentThread() let providers = withDS(thread, () => useProviderIDs()).map(id => getVM(ProviderVM, id)) LOG(`[initStorage] providers`, providers) if (!providers.some(({ type }) => ['ucan-store-proxy', 'ipfs-gateway'].includes(type))) { // TODO: replace with hasCapability('retrieve') ProviderVM.buildNew({ type: 'ipfs-gateway', name: 'IPFS.io Gateway', url: 'https://ipfs.io' }).commit(thread) } if (!providers.some(({ type, url }) => type === 'ucan-store-proxy' || url?.includes(DEMO_PROVIDER))) { // TODO: use better logic ProviderVM.buildNew({ type: 'ipfs-gateway', name: 'Note3 Demo Gateway', url: DEMO_PROVIDER }).commit(thread) } providers = withDS(thread, () => useProviderIDs()).map(id => getVM(ProviderVM, id)) // HACK refresh list after maybe adding some setStorageState({ gateways: providers.filter(({ type }) => ['ucan-store-proxy', 'ipfs-gateway'].includes(type)), }) DEBUG(`[initStorage] providers after check:`, { providers, storageState }) await initUcanUpload() setTimeout(async () => setStorageReachable(await testStorageReachability())) } export async function initUcanUpload() { const agent = useAgent() const ucanProviderIDs = useProviderIDs('ucan-store-proxy') autorunButAlsoImmediately( async () => { // HACK: we're not awaiting... // const urls = ucanProviderIDs.length && queryAndMap(withDS(getApplogDB(), () => useCurrentThread()), [ // { en: ucanProviderIDs[0], at: 'url' }, // HACK: allow multiple // ], 'vl') DEBUG(`Found UCAN storage providers?`, { providerIDs: toJS(ucanProviderIDs) }) const providers: UcanStoreProxyConnector[] = [] for (const providerID of ucanProviderIDs) { const url = getVM(ProviderVM, providerID).url if (url) { const ucanUpload = await UcanStoreProxyConnector.init({ url, issuerKey: await agent.getEdDSAsigningKeypair(), }) providers.push(ucanUpload) // ? localStorage.setItem('storage.ucan-proxy.ucan', ucan.encode(nftStorage.rootUcan)) DEBUG(`Got UcanUpload`, { ucanUpload, hasStorageSetup: agent.hasStorageSetup }) } } setStorageState({ ucanUploads: providers }) }, null, { name: 'initUcanUpload' }, ) // reaction(() => storageProviderIDs[0], () => { // LOG(`Storage provider changed`) // }) } export async function logoutNftStorage() { LOG(`Logout of NftStorage`) setStorageState({ nftStorage: null }) localStorage.setItem('storage.nftstorage.api_key', '') localStorage.setItem('storage.nftstorage.ucan', '') } export function createHandleAuthFx({ w3, email, setRegisteringSpace, tryRegisterSpace, setSubmitted, keyring }) { return async (e) => { DEBUG('Submit:', e, email()) e.preventDefault() setSubmitted(true) try { await w3.authorize(email() as `${string}@${string}`) DEBUG('Authorized!', keyring, w3) if (!keyring.spaces.length) { DEBUG('Creating space...') const did = await w3.createSpace('note3-dev') DEBUG('Created space', did) // await setCurrentSpace(did) } if (!keyring.space.registered()) { setRegisteringSpace(true) DEBUG('Space is unregistered, registering', keyring.space) let tries = 0 // HACK to work around https://github.com/web3-storage/w3ui/issues/257 const tryRegisterInLoop = async () => { try { await tryRegisterSpace() } catch (error) { console.error(`failed to register space with ${email()}:`, error) if (tries < 10) { tries++ await new Promise((resolve, reject) => setTimeout(resolve, 500 * tries)) await tryRegisterInLoop() } else { alert('Failed to register space - you can try using the \'TryFix\' button in settings') } } } await tryRegisterInLoop() } DEBUG('Got a space!', { keyring, space: keyring.space }) } catch (err) { // throw new Error('failed to authorize', { cause: err }) ERROR(`failed to authorize ${email()}`, err) notifyToast(`Failed to authorize web3storage: ${(err as any).message}`, 'danger') } finally { setRegisteringSpace(false) setSubmitted(false) } } } export const initIPNS = flow(function* initIPNS() { // TODO get array of my ipns names from localstorage #9 // TODO check if there is roothash in the URL and check if its mine #5 // if so use it, otherwise use newest from localstorage, otherwise create new // const allPublications = yield stateDB.publications.toArray() // // const pubFromIDB = (publicationThatsFocused && allPublications?.find(p => p.id === publicationThatsFocused)) || allPublications?.[0] // const agent = useAgent() // // const { agentString: appAgent, ag } = agent // const pubSigningKeyBytes = yield agent.crypto?.keystore?.publicWriteKey() // const pubEncryptionKeyBytes = yield agent.crypto?.keystore?.publicExchangeKey() // DEBUG('[odd]', { pubSigningKeyBytes, pubEncryptionKeyBytes }) // const pubKeyCryptoReimported = yield importRsaKey(pubEncryptionKeyBytes, ['encrypt']) // const enc = new TextEncoder() // const dec = new TextDecoder('utf-8') // const testPayload = enc.encode('fission is rather odd') // const encPayloadKeystore = yield agent.crypto?.rsa.encrypt(testPayload, pubKeyCryptoReimported) // const deencPayloadKeystore = dec.decode(yield agent.crypto?.keystore.decrypt(encPayloadKeystore)) // DEBUG('[odd]', { pubSigningKeyBytes, pubEncryptionKeyBytes, pubKeyCryptoReimported, encPayloadKeystore, deencPayloadKeystore }) // const exportedKey = yield state.crypto?.rsa?.exportPublicKey(pubExchangeKey) // TODO refactor this to live elsewhere // if (pubFromIDB) { // name = yield W3Name.from(pubFromIDB.pk) // yield stateDB.publications.update(name.toString(), { ts: dateNowIso(), appAgent }) // DEBUG('Loaded name: ', name.toString()) // } else { // name = yield W3Name.create() // const { key, key: { bytes: pk } } = name // const nameString = name.toString() // DEBUG('created new name: ', nameString) // //TODO: make default encrypted // yield stateDB.publications.put({ id: nameString, name: 'default', pk, createdAt: dateNowIso(), publishedBy: appAgent, lastPush: null }) // // const signedPubName = yield key.sign(name.bytes) // // const encodedSig = ab2str(signedPubName) // // insertApplogsIDB([ // // { en: nameString, at: 'pub/sig', vl: encodedSig, ag }, // // ]) // } // updateAgentState({ w3NamePublic: name }) }) // TODO make uploader a global reactively available instance // ooo sounds nice export const storeCar = action(async function storeCar(car: Blob, uploader: UploaderContextValue[1]) { try { // upload to web3.storage using putCar LOG('Uploading', car, 'using', uploader) const cid = await uploader.uploadCAR( car, /* { name: 'putCar using dag-json', // include the dag-json codec in the decoders field decoders: [json], } */ ) DEBUG('Stored dag-json:', cid.toString()) return cid } catch (err) { ERROR(`[storeCar]`, err) throw new Error(`Failed to upload: ${err?.message}\ncheck storage settings`) } }) export async function publishIPNS(cid: CidString, ipnsWritableName = name) { const value = `/ipfs/${cid}` // if we don't have a previous revision, we use Name.v0 to create the initial revision // TODO handle updating const revision = await W3Name.v0(ipnsWritableName!, value) // TODO: parallel / error handling? if (hasKubo()) { await publishIpnsToKubo(ipnsWritableName!, revision) } await W3Name.publish(revision, ipnsWritableName!.key) LOG('[w3name] published', cid, 'to', ipnsWritableName!.toString()) const parsed = CID.parse(cid) DEBUG('[w3name] parsed', parsed) // const v0 = parsed.toV0().toString() // try to get qm style cid fails // DEBUG('[w3name] v0', v0, `https://explore.ipld.io/#/explore/${v0}`) } // TODO make some more sexy store or state or signalDepot or something export const getW3NamePublic = () => useAgent().w3NamePublic?.toString()