diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c692c05..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/src/fileAPI.ts b/src/fileAPI.ts index 5511425..8ffdf26 100644 --- a/src/fileAPI.ts +++ b/src/fileAPI.ts @@ -285,4 +285,15 @@ export class FileAPI { } return users.filter((d) => d !== ethers.constants.AddressZero) } + + @requiresLocking + private async _addFile (did: string) { + await this.setAppAddress(did) + return await makeTx(this.appAddress, this.api, this.provider, 'addFile', [did]) + } + + addFile = async (did: string) => { + const realDID = parseHex(did) + return this._addFile(realDID) + } } diff --git a/tests/test.ts b/tests/test.ts index 2c2b235..11467fc 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -1,14 +1,14 @@ -import test from 'ava' -import sinon from 'sinon' -import { BigNumber, ethers, Wallet } from 'ethers' -import { createEngine } from './sub_provider' -import fs from 'fs' -import nock from 'nock' -import { Blob as nBlob } from 'blob-polyfill' -import axios from 'axios' -import httpAdapter from 'axios/lib/adapters/http' - -import { providerFromEngine } from 'eth-json-rpc-middleware' +import test from 'ava'; +import sinon from 'sinon'; +import { BigNumber, ethers, Wallet } from 'ethers'; +import { createEngine } from './sub_provider'; +import fs from 'fs'; +import nock from 'nock'; +import { Blob as nBlob } from 'blob-polyfill'; +import axios from 'axios'; +import httpAdapter from 'axios/lib/adapters/http'; + +import { providerFromEngine } from 'eth-json-rpc-middleware'; // SDK imports import { StorageProvider } from '../src' @@ -17,8 +17,8 @@ import { parseData } from './utils' import { CustomError } from '../src/types' import DID from '../src/contracts/DID' // Load contract addresses -const sContracts: any = fs.readFileSync('./tests/contracts.json') -const oContracts = JSON.parse(sContracts) +const sContracts: any = fs.readFileSync('./tests/contracts.json'); +const oContracts = JSON.parse(sContracts); const gateway = 'http://localhost:9010/' const appId = 1 @@ -26,8 +26,8 @@ const appAddress = '445007f942f9Ba718953094Bbe3205B9484cAfd2' const debug = false // To ignore strict http request/response rules -axios.defaults.adapter = httpAdapter -const nockOptions = { 'Access-Control-Allow-Origin': '*' } +axios.defaults.adapter = httpAdapter; +const nockOptions = { 'Access-Control-Allow-Origin': '*' }; /* Not using moxis because of axios instance initialization in SDK @@ -35,34 +35,34 @@ Below to be covered in Integration Tests -> Download (due to tus client instance) */ -function sleep (ms) { +function sleep(ms) { return new Promise((resolve) => { - setTimeout(resolve, ms) - }) + setTimeout(resolve, ms); + }); } const makeEmail = () => { - const strValues = 'abcdefg12345' - let strEmail = '' - let strTmp + const strValues = 'abcdefg12345'; + let strEmail = ''; + let strTmp; for (let i = 0; i < 10; i++) { - strTmp = strValues.charAt(Math.round(strValues.length * Math.random())) - strEmail = strEmail + strTmp + strTmp = strValues.charAt(Math.round(strValues.length * Math.random())); + strEmail = strEmail + strTmp; } - strTmp = '' - strEmail = strEmail + '@example.com' - return strEmail -} + strTmp = ''; + strEmail = strEmail + '@example.com'; + return strEmail; +}; -let file +let file; // arcanaInstance, // receiverInstance, -const did = '0x4de0e96b0a8886e42a2c35b57df8a9d58a93b5bff655bc37a30e2ab8e29dc066' +const did = '0x4de0e96b0a8886e42a2c35b57df8a9d58a93b5bff655bc37a30e2ab8e29dc066'; -function meta_tx_nock (reply_data) { +function meta_tx_nock(reply_data) { const nockMetaReply = async (uri, body: any) => { - return reply_data ?? { data: 'dummy data', token: 'dummy token' } - } + return reply_data ?? { data: 'dummy data', token: 'dummy token' }; + }; nock(gateway) .defaultReplyHeaders(nockOptions) @@ -72,15 +72,15 @@ function meta_tx_nock (reply_data) { .reply(200, nockMetaReply, { 'access-control-allow-headers': 'Authorization' }) } -function mock_dkg (reply_data) { +function mock_dkg(reply_data) { nock('https://dkgnode1.arcana.network:443') .defaultReplyHeaders(nockOptions) .post('/rpc') .times(6) - .reply(200, { jsonrpc: '2.0', result: { ok: true }, id: 10 }) + .reply(200, { jsonrpc: '2.0', result: { ok: true }, id: 10 }); } -async function nockSetup () { +async function nockSetup() { nock('http://localhost:9010') .defaultReplyHeaders(nockOptions) .persist() @@ -90,7 +90,7 @@ async function nockSetup () { Forwarder: oContracts.Forwarder, RPC_URL: 'http://localhost:10002', DID: oContracts.DID, - DKG: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' + DKG: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', }) .post('/api/v1/login/') .reply(200, { token: '123456789' }) @@ -105,99 +105,99 @@ async function nockSetup () { .reply(200, { address: oContracts.App }, { 'access-control-allow-headers': 'Authorization' }) .get('/api/v1/get-node-address/') .query(true) - .reply(200, { host: 'http://localhost:3000/', address: storage_node.address }) + .reply(200, { host: 'http://localhost:3000/', address: storage_node.address }); nock('http://localhost:3000') .persist() .defaultReplyHeaders(nockOptions) .patch((p) => p.startsWith('/api/v2/file/')) .reply(200, { - hash: '0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e' + hash: '0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e', }) .post((p) => p.startsWith('/api/v2/file/')) - .reply(200, {}) + .reply(200, {}); } -function sinonMockObjectSetup () { +function sinonMockObjectSetup() { sinon.replace(utils, 'getDKGNodes', () => [ { declaredIp: 'dkgnode1.arcana.network:443', position: '1', pubKx: BigNumber.from('29023421385368379144749466045924017514934229958180852799451398628000593771667'), - pubKy: BigNumber.from('31632158778368581637676511185062566059198308712876704725543144993632262155464') + pubKy: BigNumber.from('31632158778368581637676511185062566059198308712876704725543144993632262155464'), }, { declaredIp: 'dkgnode1.arcana.network:443', position: '2', pubKx: BigNumber.from('105719267757522549686383951453889518570805320580847799971673920448991999863268'), - pubKy: BigNumber.from('12311889399951856112539425386359305279151271210811891657961588078446721210801') + pubKy: BigNumber.from('12311889399951856112539425386359305279151271210811891657961588078446721210801'), }, { declaredIp: 'dkgnode1.arcana.network:443', position: '3', pubKx: BigNumber.from('112513454780213693752054630002769173645973927254986348958538391710171734325064'), - pubKy: BigNumber.from('31826403948237730820406540123018982546704465196666925150128355254483964682271') + pubKy: BigNumber.from('31826403948237730820406540123018982546704465196666925150128355254483964682271'), }, { declaredIp: 'dkgnode1.arcana.network:443', position: '4', pubKx: BigNumber.from('103022124116237959935952092341458720857383888117879935947184525301185593633427'), - pubKy: BigNumber.from('83428276264331813311663241272832111383329363811859329412601611536906464022186') + pubKy: BigNumber.from('83428276264331813311663241272832111383329363811859329412601611536906464022186'), }, { declaredIp: 'dkgnode1.arcana.network:443', position: '5', pubKx: BigNumber.from('72082384183905358797739369765923546941331333550297636524350044306990429216270'), - pubKy: BigNumber.from('661783827736034504670612788123848346528644035307464845748154787461466575102') + pubKy: BigNumber.from('661783827736034504670612788123848346528644035307464845748154787461466575102'), }, { declaredIp: 'dkgnode1.arcana.network:443', position: '6', pubKx: BigNumber.from('30438236858857419456992904193833033911277657186396590512267279659738218054034'), - pubKy: BigNumber.from('27076479865999379327196017777333283283075678191787288284998453473449446886409') - } - ]) - sinon.replace(utils, 'checkTxnStatus', () => Promise.resolve()) - sinon.replace(utils, 'getFile', () => Promise.resolve({ app: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' })) + pubKy: BigNumber.from('27076479865999379327196017777333283283075678191787288284998453473449446886409'), + }, + ]); + sinon.replace(utils, 'checkTxnStatus', () => Promise.resolve()); + sinon.replace(utils, 'getFile', () => Promise.resolve({ app: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' })); } -async function mockFile () { +async function mockFile() { // file = MockFile('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.txt', 2 ** 10, 'image/txt'); // file = new File([file], "picsum_img", { type: file.type }); - return new nBlob([await (await fetch('https://picsum.photos/id/872/200/300')).arrayBuffer()]) + return new nBlob([await (await fetch('https://picsum.photos/id/872/200/300')).arrayBuffer()]); } // Wallet Setup -const memonic = 'test test test test test test test test test test test junk' -const path = "m/44'/60'/0'/0/" +const memonic = 'test test test test test test test test test test test junk'; +const path = "m/44'/60'/0'/0/"; -const deployer = ethers.Wallet.fromMnemonic(memonic, path + '0') -const gateway_node = ethers.Wallet.fromMnemonic(memonic, path + '1') -const storage_node = ethers.Wallet.fromMnemonic(memonic, path + '2') -const bridge = ethers.Wallet.fromMnemonic(memonic, path + '3') -const arcanaWallet = ethers.Wallet.fromMnemonic(memonic, path + '4') -const receiverWallet = ethers.Wallet.fromMnemonic(memonic, path + '5') +const deployer = ethers.Wallet.fromMnemonic(memonic, path + '0'); +const gateway_node = ethers.Wallet.fromMnemonic(memonic, path + '1'); +const storage_node = ethers.Wallet.fromMnemonic(memonic, path + '2'); +const bridge = ethers.Wallet.fromMnemonic(memonic, path + '3'); +const arcanaWallet = ethers.Wallet.fromMnemonic(memonic, path + '4'); +const receiverWallet = ethers.Wallet.fromMnemonic(memonic, path + '5'); // shared fixture -let fixture +let fixture; // Mock server & stub setup test.serial.before(async (t) => { // Mock gateway response(s) setup - nockSetup() + nockSetup(); // Mock basic Storage utils - sinonMockObjectSetup() + sinonMockObjectSetup(); // File prep - file = await mockFile() - file.name = 'test_file' -}) + file = await mockFile(); + file.name = 'test_file'; +}); // TODO: get request handler in arrays -async function createStorageInstance (wallet: Wallet, middleware?) { - const engine = createEngine(wallet.address) +async function createStorageInstance(wallet: Wallet, middleware?) { + const engine = createEngine(wallet.address); if (middleware) { - engine.push(middleware) + engine.push(middleware); } const instance = await StorageProvider.init({ @@ -206,14 +206,14 @@ async function createStorageInstance (wallet: Wallet, middleware?) { gateway, debug, chainId: 100, - provider: providerFromEngine(engine) - }) + provider: providerFromEngine(engine), + }); - return Promise.resolve(instance) + return Promise.resolve(instance); } test.serial('Upload file', async (t) => { - meta_tx_nock(undefined) + meta_tx_nock(undefined); const arcanaInstance = await createStorageInstance(arcanaWallet, (req, res, next, end) => { if (req.method === 'eth_getTransactionByHash') { @@ -233,8 +233,8 @@ test.serial('Upload file', async (t) => { transactionIndex: '0x1', type: '0x0', v: '0x1c', - value: '0x6113a84987be800' - } + value: '0x6113a84987be800', + }; } else if (req.method === 'eth_getTransactionReceipt') { res.result = { transactionHash: '0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e', @@ -251,19 +251,19 @@ test.serial('Upload file', async (t) => { status: '0x1', to: '0xdf190dc7190dfba737d7777a163445b7fff16133', transactionIndex: '0x1', - type: '0x0' - } + type: '0x0', + }; } - end() - }) + end(); + }); - const upload = await arcanaInstance.getUploader() - await t.notThrowsAsync(upload.upload(file)) -}) + const upload = await arcanaInstance.getUploader(); + await t.notThrowsAsync(upload.upload(file)); +}); test.skip('Download file', async (t) => { // unable to fake key shares responses -}) +}); test.serial.skip('Metadata URL', async (t) => { // Skipped because axios need additional transformation for nodejs env @@ -272,8 +272,8 @@ test.serial.skip('Metadata URL', async (t) => { .defaultReplyHeaders(nockOptions) .post('/api/v1/nft') .reply(200, (req) => { - Promise.resolve({ data: { request: { responseURL: 'dummy.image.url' } } }) - }) + Promise.resolve({ data: { request: { responseURL: 'dummy.image.url' } } }); + }); nock(gateway) .post('/api/v1/metadata') @@ -281,32 +281,32 @@ test.serial.skip('Metadata URL', async (t) => { const arcanaInstance = await createStorageInstance(arcanaWallet) const metadataURL = await arcanaInstance.makeMetadataURL('test', 'test description', did, file) - t.is(metadataURL, 'dummy.metadata.url'.concat('/', did)) -}) + t.is(metadataURL, 'dummy.metadata.url'.concat('/', did)); +}); test.serial('Share file', async (t) => { - t.plan(4) - meta_tx_nock(undefined) + t.plan(4); + meta_tx_nock(undefined); const middleware = (req, res, next, end) => { const data = parseData( { value: ethers.utils.parseEther('0'), - data: req.params[0].data + data: req.params[0].data, }, - DID.abi - ) + DID.abi, + ); switch (data.name) { case 'getRuleSet': - res.result = ethers.constants.HashZero + res.result = ethers.constants.HashZero; } - end() - } + end(); + }; - const arcanaInstance = await createStorageInstance(arcanaWallet, middleware) + const arcanaInstance = await createStorageInstance(arcanaWallet, middleware); - const access = await arcanaInstance.getAccess() + const access = await arcanaInstance.getAccess(); // Now check whether it showing in receipt user list nock(gateway) @@ -321,29 +321,29 @@ test.serial('Share file', async (t) => { .post('/api/v1/update-hash/') .reply(200, {}) - const tx = await access.share(did, [receiverWallet.address], [150]) - t.truthy(tx) + const tx = await access.share(did, [receiverWallet.address], [150]); + t.truthy(tx); - const receiverInstance = await createStorageInstance(receiverWallet) + const receiverInstance = await createStorageInstance(receiverWallet); - const files = await receiverInstance.sharedFiles() - t.is(files.length, 1) - t.is(files[0].did, did.substring(2)) - t.is(files[0].size, file.size) -}) + const files = await receiverInstance.sharedFiles(); + t.is(files.length, 1); + t.is(files[0].did, did.substring(2)); + t.is(files[0].size, file.size); +}); test.serial('Fail revoke transaction on unauthorized files', async (t) => { - t.plan(3) - const expected_errorCode = 'You dont have access to perform this operation' + t.plan(3); + const expected_errorCode = 'You dont have access to perform this operation'; nock(gateway) .defaultReplyHeaders(nockOptions) .post('/api/v1/update-hash/') .reply(200, { - err: expected_errorCode + err: expected_errorCode, }) .get('/api/v1/get-hash-data/') .query(true) - .reply(200, null) + .reply(200, null); const middleware = (req, res, next, end) => { switch (req.method) { @@ -354,87 +354,87 @@ test.serial('Fail revoke transaction on unauthorized files', async (t) => { ethers.BigNumber.from('120000'), true, ethers.utils.randomBytes(120), - ethers.utils.id('random_address').substring(0, 42) - ] - ) - break + ethers.utils.id('random_address').substring(0, 42), + ], + ); + break; } } - end() - } - const receiverInstance = await createStorageInstance(receiverWallet, middleware) - const err = (await t.throwsAsync(receiverInstance.files.revoke(did, arcanaWallet.address))) as CustomError - t.true(err.message.endsWith(expected_errorCode)) - t.assert(err.code.startsWith('TRANSACTION')) -}) + end(); + }; + const receiverInstance = await createStorageInstance(receiverWallet, middleware); + const err = (await t.throwsAsync(receiverInstance.files.revoke(did, arcanaWallet.address))) as CustomError; + t.true(err.message.endsWith(expected_errorCode)); + t.assert(err.code.startsWith('TRANSACTION')); +}); test.serial('Get consumed and total upload limit', async (t) => { const middleware = (req, res, next, end) => { const data = parseData({ value: ethers.utils.parseEther('0'), - data: req.params[0].data - }) + data: req.params[0].data, + }); switch (data.name) { case 'limit': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]); + break; case 'consumption': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [file.size, 0]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [file.size, 0]); + break; case 'defaultLimit': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]); + break; } - end() - } + end(); + }; - const arcanaInstance = await createStorageInstance(arcanaWallet, middleware) - const Access = await arcanaInstance.getAccess() - const [consumed, total] = await Access.getUploadLimit() - t.is(consumed, file.size) - t.is(total, 100000000) -}) + const arcanaInstance = await createStorageInstance(arcanaWallet, middleware); + const Access = await arcanaInstance.getAccess(); + const [consumed, total] = await Access.getUploadLimit(); + t.is(consumed, file.size); + t.is(total, 100000000); +}); test.serial('Get consumed and total download limit', async (t) => { const middleware = (req, res, next, end) => { const data = parseData({ value: ethers.utils.parseEther('0'), - data: req.params[0].data - }) + data: req.params[0].data, + }); switch (data.name) { case 'limit': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]); + break; case 'consumption': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [0, file.size]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [0, file.size]); + break; case 'defaultLimit': - res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint', 'uint'], [100000000, 100000000]); + break; } - end() - } + end(); + }; - const arcanaInstance = await createStorageInstance(arcanaWallet, middleware) - const Access = await arcanaInstance.getAccess() - const [consumed, total] = await Access.getDownloadLimit() - t.is(consumed, file.size) - t.is(total, 100000000) -}) + const arcanaInstance = await createStorageInstance(arcanaWallet, middleware); + const Access = await arcanaInstance.getAccess(); + const [consumed, total] = await Access.getDownloadLimit(); + t.is(consumed, file.size); + t.is(total, 100000000); +}); test.serial('Revoke', async (t) => { - meta_tx_nock(null) + meta_tx_nock(null); let middleware = (req, res, next, end) => { if (req.method == 'eth_call') { - res.result = ethers.utils.defaultAbiCoder.encode(['address[]'], [[receiverWallet.address]]) + res.result = ethers.utils.defaultAbiCoder.encode(['address[]'], [[receiverWallet.address]]); } - end() - } + end(); + }; - let arcanaInstance = await createStorageInstance(arcanaWallet, middleware) + let arcanaInstance = await createStorageInstance(arcanaWallet, middleware); - let access = await arcanaInstance.getAccess() + let access = await arcanaInstance.getAccess(); nock(gateway) .defaultReplyHeaders(nockOptions) .get('/api/v1/shared-users/?did=' + did) @@ -443,24 +443,24 @@ test.serial('Revoke', async (t) => { .reply(200, {}) .get('/api/v1/get-hash-data/') .query(true) - .reply(200, null) + .reply(200, null); - const beforeRevokeUsers = await access.getSharedUsers(did) - const tx = await access.revoke(did, receiverWallet.address) - t.truthy(tx) + const beforeRevokeUsers = await access.getSharedUsers(did); + const tx = await access.revoke(did, receiverWallet.address); + t.truthy(tx); middleware = (req, res, next, end) => { if (req.method == 'eth_call') { switch (true) { case req.params[0].data.startsWith('0x6184533f'): - res.result = ethers.utils.defaultAbiCoder.encode(['address[]'], [[]]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['address[]'], [[]]); + break; } } - end() + end(); }; - (arcanaInstance = await createStorageInstance(arcanaWallet, middleware)), (access = await arcanaInstance.getAccess()) + (arcanaInstance = await createStorageInstance(arcanaWallet, middleware)), (access = await arcanaInstance.getAccess()); nock(gateway) .defaultReplyHeaders(nockOptions) @@ -468,9 +468,9 @@ test.serial('Revoke', async (t) => { .reply(200, [], { 'access-control-allow-headers': 'Authorization' }) const afterRevokeUsers = await access.getSharedUsers(did) - t.is(beforeRevokeUsers.includes(receiverWallet.address), true) - t.is(afterRevokeUsers.includes(receiverWallet.address), false) - t.is(beforeRevokeUsers.length - afterRevokeUsers.length, 1) + t.is(beforeRevokeUsers.includes(receiverWallet.address), true); + t.is(afterRevokeUsers.includes(receiverWallet.address), false); + t.is(beforeRevokeUsers.length - afterRevokeUsers.length, 1); await nock(gateway) .defaultReplyHeaders(nockOptions) @@ -480,14 +480,14 @@ test.serial('Revoke', async (t) => { .get('/api/v1/files/shared/total/') .reply(200, { data: 0 }) - const receiverInstance = await createStorageInstance(receiverWallet) + const receiverInstance = await createStorageInstance(receiverWallet); - const files = await receiverInstance.sharedFiles() - t.is(files.length, 0) -}) + const files = await receiverInstance.sharedFiles(); + t.is(files.length, 0); +}); test.serial('Delete File', async (t) => { - meta_tx_nock(null) + meta_tx_nock(null); const scope = nock(gateway) .defaultReplyHeaders(nockOptions) @@ -497,16 +497,16 @@ test.serial('Delete File', async (t) => { .get('/api/v1/files/total/') .reply(200, { data: 1 }) - const arcanaInstance = await createStorageInstance(arcanaWallet) + const arcanaInstance = await createStorageInstance(arcanaWallet); - const access = await arcanaInstance.getAccess() + const access = await arcanaInstance.getAccess(); - let files = await arcanaInstance.myFiles() + let files = await arcanaInstance.myFiles(); - t.is(files.length, 1) - t.is(files[0].did, did.substring(2)) + t.is(files.length, 1); + t.is(files[0].did, did.substring(2)); - const tx = await access.deleteFile(did) + const tx = await access.deleteFile(did); nock(gateway) .defaultReplyHeaders(nockOptions) @@ -516,30 +516,104 @@ test.serial('Delete File', async (t) => { .get('/api/v1/files/total/') .reply(200, { data: 0 }) - files = await arcanaInstance.myFiles() + files = await arcanaInstance.myFiles(); - t.is(files.length, 0) - t.truthy(tx) -}) + t.is(files.length, 0); + t.truthy(tx); +}); test.serial('Grant app permission', async (t) => { - meta_tx_nock(null) + meta_tx_nock(null); const middleware = (req, res, next, end) => { const data = parseData({ value: ethers.utils.parseEther('0'), - data: req.params[0].data - }) + data: req.params[0].data, + }); switch (data.name) { case 'appLevelControl': - res.result = ethers.utils.defaultAbiCoder.encode(['uint8'], [1]) - break + res.result = ethers.utils.defaultAbiCoder.encode(['uint8'], [1]); + break; case 'userAppPermission': - res.result = ethers.utils.defaultAbiCoder.encode(['uint8'], [0]) + res.result = ethers.utils.defaultAbiCoder.encode(['uint8'], [0]); } - end() - } + end(); + }; + + const arcanaInstance = await createStorageInstance(arcanaWallet, middleware); + await t.notThrowsAsync(arcanaInstance.grantAppPermission()); +}); + +test.serial('Add file to app', async (t) => { + t.plan(7); + meta_tx_nock(null); + let scope = nock(gateway) + .defaultReplyHeaders(nockOptions) + .get('/api/v1/list-files/') + .query(true) + .reply(200, [{ did: did.substring(2) }], { 'access-control-allow-headers': 'Authorization' }) + .get('/api/v1/files/total/') + .reply(200, { data: 1 }); + + const arcanaInstance = await createStorageInstance(arcanaWallet); + + let files: any = await arcanaInstance.myFiles(); + + t.true(scope.isDone()); + t.is(files.length, 1); + t.is(files[0].did, did.substring(2)); + + const did2 = '0x4de0e96b0a8886e42a2c35b57df8a9d58a93b5bff655bc37a30e2ab8e29dc066'; + + await t.notThrowsAsync(arcanaInstance.files.addFile(did2)); + + scope = nock(gateway) + .defaultReplyHeaders(nockOptions) + .get('/api/v1/list-files/') + .query(true) + .reply(200, [{ did: did.substring(2) }, { did: did2.substring(2) }], { + 'access-control-allow-headers': 'Authorization', + }) + .get('/api/v1/files/total/') + .reply(200, { data: 2 }); + + files = await arcanaInstance.myFiles(); + t.true(scope.isDone()); + t.is(files.length, 2); + t.is(files[1].did, did2.substring(2)); +}); + +test.serial('Remove file from app', async (t) => { + t.plan(6); + meta_tx_nock(null); + let scope = nock(gateway) + .defaultReplyHeaders(nockOptions) + .get('/api/v1/list-files/') + .query(true) + .reply(200, [{ did: did.substring(2) }], { 'access-control-allow-headers': 'Authorization' }) + .get('/api/v1/files/total/') + .reply(200, { data: 1 }); + + const arcanaInstance = await createStorageInstance(arcanaWallet); + + let files: any = await arcanaInstance.myFiles(); + + t.true(scope.isDone()); + t.is(files.length, 1); + t.is(files[0].did, did.substring(2)); + + // const access = await arcanaInstance.getAccess(); + await t.notThrowsAsync(arcanaInstance.files.removeFileFromApp(did)); + + scope = nock(gateway) + .defaultReplyHeaders(nockOptions) + .get('/api/v1/list-files/') + .query(true) + .reply(200, [], { 'access-control-allow-headers': 'Authorization' }) + .get('/api/v1/files/total/') + .reply(200, { data: 0 }); - const arcanaInstance = await createStorageInstance(arcanaWallet, middleware) - await t.notThrowsAsync(arcanaInstance.grantAppPermission()) -}) + files = await arcanaInstance.myFiles(); + t.true(scope.isDone()); + t.is(files.length, 0); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 8bfa87d..e0ec7da 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,7 +1,7 @@ -import { ethers } from 'ethers' -import arcana from '../src/contracts/Arcana' +import { ethers } from 'ethers'; +import arcana from '../src/contracts/Arcana'; export const parseData = (data: any, abi?: any[]) => { - const iface = new ethers.utils.Interface(abi ?? arcana.abi) - const pt = iface.parseTransaction(data) - return { name: pt.functionFragment.name, args: pt.args } -} + const iface = new ethers.utils.Interface(abi ?? arcana.abi); + const pt = iface.parseTransaction(data); + return { name: pt.functionFragment.name, args: pt.args }; +}; diff --git a/usage.md b/usage.md index cd47976..b54c707 100644 --- a/usage.md +++ b/usage.md @@ -27,9 +27,9 @@ Refer to the [Arcana Storage SDK Quick Start Guide](https://docs.beta.arcana.net ## Usage Flow 1. Install Storage SDK -2. Import `StorageProvider` from the Storage SDK package in the dApp. Call `init` method of `StorageProvider` and specify the Web3 wallet `provider` and the `appId` as input parameters. **Note:** Get the provider via the Auth SDK or third-party supported wallet. You can copy the appId from the [Arcana Developer Dashboard](https://docs.beta.arcana.network/docs/config_dapp) after registering your dApp +2. Import `StorageProvider` from the Storage SDK package in the dApp. Call the `init` method of `StorageProvider` and specify the Web3 wallet `provider` and the `appAddress` as input parameters. **Note:** Get the provider via the Auth SDK or third-party supported wallet. You can copy the **App Address** from the [Arcana Developer Dashboard](https://docs.beta.arcana.network/docs/config_dapp) after registering your dApp. In the earlier releases, **App Address** was referred to as **App ID** in the dashboard. 3. Use `StorageProvider` to: - - `upload` and push file data into the Arcana Store. **Note:** Save file DID that is returned after file upload operation is successful. + - `upload` and push file data into the Arcana Store. **Note:** Save file DID that is returned after the successful file upload. - `download` a file from the Arcana Store using DID as input. 4. Use `StorageProvider.files` to: - `delete` a file by specifying its DID. @@ -65,7 +65,7 @@ This *Singleton* usage is recommended as a best practice. The Storage SDK accepts `Blob`s as files. The `file` object passed must be an instance of a `Blob` or a descendant (`File`, etc.). You cannot upload a file by providing its URL. -As of now, it supports uploading _private_ and _public_ files. They are identifiable by looking at the first byte of the DID. In hexadecimal format, 01 indicates it's a public file, and 02 indicates it's a private file. +As of now, it supports uploading _private_ and _public_ files. They are identifiable by looking at the first byte of the DID. In hexadecimal format, 01 indicates a public file, and 02 indicates a private file. ### Private Files @@ -117,7 +117,7 @@ await dAppStorageProvider.download( ### Share a File ```ts -// did: DID of file to be shared +// did: DID of the file to be shared // address: recipient user's address // validity (optional): For how long will the user be able to download the file, e.g. [400] would mean 400 seconds await dAppStorageProvider.files.share([did], [address]); @@ -127,7 +127,7 @@ await dAppStorageProvider.files.share([did], [address]); ```ts // did: DID of file from which access is removed -// address: Address of the user for whom the access must be revoked +// address: The address of the user for whom the access must be revoked await dAppStorageProvider.files.revoke(did, address); ``` @@ -156,7 +156,7 @@ let [consumed, total] = await dAppStorageProvider.files.getUploadLimit(); ### Get Download Limit ```ts -//Get consumed and total bandwidth of the current user +//Get consumed and the total bandwidth of the current user let [consumed, total] = await dAppStorageProvider.files.getDownloadLimit(); ``` @@ -183,7 +183,8 @@ let files = await dAppStorageProvider.files.list(AccessTypeEnum.MY_FILES); ```ts //The file DID is returned at the time of file upload and uniquely identifies the file in Arcana Store. -//Note: No appID is required during initialization of the Storage SDK in order to +//Note: No **App Address** needs to be specified during the initialization of the Storage SDK +//if a dApp only requires to //download a file using the file DID. // Pass the provider during initialization of the Storage SDK, if required. @@ -213,7 +214,7 @@ let metadata = await dAppStorageProvider.makeMetadataURL( title, description, did, // The DID of the private NFT file hosted in the Arcana Store - file, // The 'preview image' file corresponding to the private NFT, not the actual private NFT data file + file, // The 'preview image file corresponding to the private NFT, not the actual private NFT data file ); console.log(metadata); // https://test-storage.arcana.network:9000/api/v1/metadata/0x129d1438ff3bf014e9b9094b3a5d410f691c208ed5305b0844307b761c0e295e @@ -221,7 +222,7 @@ console.log(metadata); ### Link Minted NFT with DID -Once you have minted the NFT, to make it private and control access to it and manage ownership, you need to link it with the DID. +Once you have minted the NFT, you need to link it with the DID to make it private, control access, and manage ownership. ```ts let chainId = 80001,tokenId = 3, nftContract = "0xE80FCAD702b72777f5036eF1a76086FD3f882E29" @@ -269,20 +270,47 @@ dAppStorageProvider.onAccountChange = (accounts) => { } ``` +## Delegate Data Access Permissions -## App permissions +A delegate may perform data access operations as per the access rights granted to them by the data owner. For example, a moderator reviewing a stream of tweets for a decentralized dApp may be granted permissions to delete objectionable tweets, or simply flag them. -App might require to pass additional permissions for app delegates. +The following APIs support data access permission delegation. -### Grant App permissions +**Note** +In the current release, a dApp user can delegate data access permissions to a dApp developer, if they choose to. In the future, we may support third-party services that take on the delegation role. + +### Grant Delegator Permission to dApp + +This API can be used by a dApp to seek the user's permission to get the role of a delegate with data access control on behalf of the data owner. ```js await storage.grantAppPermission() ``` -### Check whether user needs to grant permissions to the app. -returns `true` if required. +### Check if dAPP requires Delegator Permission + +The dApp can use this API to check if it needs to seek delegator permissions from the dApp user. It returns `true` if dApp requires permission. ```js const isPermissionRequired = await storage.checkPermission() -> boolean -``` \ No newline at end of file +``` + +## Inject/Eject Data + +The inject/eject APIs empower the dApp users to own their data. A dApp user can choose to upload data to the Arcana Store via a dApp that is integrated with the Arcana Storage SDK. Later, they can use another dApp integrated with the Storage SDK and access the same data uploaded using the previous dApp into the Arcana Store. This is done by injecting or adding the data file into the new dApp context. Similarly, dApp users can eject or remove a data file that was uploaded using a dApp. If a data file is removed from the context of a dApp, the same user cannot access the file from that dApp. The removal from a dApp context does not delete the file from the Arcana Store. + +### Add File to dApp + +A dApp user can upload files to the Arcana Store. To access the same file from a different dApp, the file must be injected in the new dApp context. Use this API to add an already uploaded file to a new dApp context. + +```js +await storage.files.addFile() +``` + +### Remove File from dApp + +Whenever a dApp user uploads a file using a dApp or uses the `addFile` API for an already uploaded file, the data file gets added to the dApp context. This enables a user to access a file uploaded using any dApp from more than one dApp context. To stop file access, the file must be ejected from the dApp's context. Use `removeFile` API to remove it from a dApp's context. The removal of the file(s) does **NOT** delete the file(s) from the Arcana storage. To delete the file, use the `delete` API. + +```js +await storage.files.removeFileFromApp() +```