diff --git a/src/messages/store/index.ts b/src/messages/store/index.ts index 58c3a56a..e8ab4018 100644 --- a/src/messages/store/index.ts +++ b/src/messages/store/index.ts @@ -1,4 +1,5 @@ import { Get } from "./get"; import { Publish } from "./publish"; +import { Pin } from "./pin"; -export { Get, Publish }; +export { Get, Publish, Pin }; diff --git a/src/messages/store/pin.ts b/src/messages/store/pin.ts new file mode 100644 index 00000000..27bb0ff3 --- /dev/null +++ b/src/messages/store/pin.ts @@ -0,0 +1,28 @@ +import * as base from "../../accounts/account"; +import { ItemType, StoreMessage } from "../message"; +import { Publish } from "./publish"; +import { DEFAULT_API_V2 } from "../../global"; + +type StorePinConfiguration = { + channel: string; + account: base.Account; + fileHash: string; + storageEngine?: ItemType; + APIServer?: string; +}; + +/** + * Publishes a store message, containing a hash to pin an IPFS file. + * You also have to provide default message properties, such as the targeted channel or the account used to sign the message. + * + * @param spc The configuration used to pin the file. + */ +export async function Pin(spc: StorePinConfiguration): Promise { + return await Publish({ + account: spc.account, + channel: spc.channel, + fileHash: spc.fileHash, + APIServer: spc.APIServer || DEFAULT_API_V2, + storageEngine: ItemType.ipfs, + }); +} diff --git a/src/messages/store/publish.ts b/src/messages/store/publish.ts index 70c4e1b5..00d712ed 100644 --- a/src/messages/store/publish.ts +++ b/src/messages/store/publish.ts @@ -1,12 +1,14 @@ import * as base from "../../accounts/account"; -import { MessageType, ItemType, StoreContent, StoreMessage } from "../message"; +import { ItemType, MessageType, StoreContent, StoreMessage } from "../message"; import { PushFileToStorageEngine, PutContentToStorageEngine } from "../create/publish"; import { SignAndBroadcast } from "../create/signature"; +import { RequireOnlyOne } from "../../utils/requiredOnlyOne"; type StorePublishConfiguration = { channel: string; account: base.Account; - fileObject: Buffer | Blob; + fileObject?: Buffer | Blob; + fileHash?: string; storageEngine: ItemType; APIServer: string; }; @@ -17,12 +19,22 @@ type StorePublishConfiguration = { * * @param spc The configuration used to publish a store message. */ -export async function Publish(spc: StorePublishConfiguration): Promise { - const hash = await PushFileToStorageEngine({ - APIServer: spc.APIServer, - storageEngine: spc.storageEngine, - file: spc.fileObject, - }); +export async function Publish( + spc: RequireOnlyOne, +): Promise { + if (!spc.fileObject && !spc.fileHash) throw new Error("You need to specify a File to upload or a Hash to pin."); + if (spc.fileObject && spc.fileHash) throw new Error("You can't pin a file and upload it at the same time."); + if (spc.fileHash && spc.storageEngine !== ItemType.ipfs) throw new Error("You must choose ipfs to pin file."); + + const hash = + spc.fileHash || + (await PushFileToStorageEngine({ + APIServer: spc.APIServer, + storageEngine: spc.storageEngine, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + file: spc.fileObject, + })); const timestamp = Date.now() / 1000; const content: StoreContent = { diff --git a/src/utils/requiredOnlyOne.ts b/src/utils/requiredOnlyOne.ts new file mode 100644 index 00000000..384c4d29 --- /dev/null +++ b/src/utils/requiredOnlyOne.ts @@ -0,0 +1,8 @@ +/** + * This type implementation allows to specify for a given T type two field with only one that have to be field. + * You can find more about this here: https://learn.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest + */ +export type RequireOnlyOne = Pick> & + { + [K in Keys]-?: Required> & Partial, undefined>>; + }[Keys]; diff --git a/tests/messages/store/publish.test.ts b/tests/messages/store/publish.test.ts index ec4f6501..d9f7a562 100644 --- a/tests/messages/store/publish.test.ts +++ b/tests/messages/store/publish.test.ts @@ -31,4 +31,68 @@ describe("Store message publish", () => { expect(got).toBe(expected); }); + + it("should pin a file and retrieve it correctly", async () => { + const mnemonic = "twenty enough win warrior then fiction smoke tenant juice lift palace inherit"; + const account = ethereum.ImportAccountFromMnemonic(mnemonic); + const helloWorldHash = "QmTp2hEo8eXRp6wg7jXv1BLCMh5a4F3B7buAUZNZUu772j"; + + const hash = await store.Pin({ + channel: "TEST", + account: account, + fileHash: helloWorldHash, + }); + + const response = await store.Get({ + fileHash: hash.content.item_hash, + APIServer: DEFAULT_API_V2, + }); + + const got = ArraybufferToString(response); + const expected = "hello world!"; + + expect(got).toBe(expected); + }); + + it("should fail to pin a file at runtime", async () => { + const mnemonic = "twenty enough win warrior then fiction smoke tenant juice lift palace inherit"; + const account = ethereum.ImportAccountFromMnemonic(mnemonic); + + const helloWorldHash = "QmTp2hEo8eXRp6wg7jXv1BLCMh5a4F3B7buAUZNZUu772j"; + const fileContent = readFileSync("./tests/messages/store/testFile.txt"); + + await expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store.Publish({ + channel: "TEST", + APIServer: DEFAULT_API_V2, + account: account, + storageEngine: ItemType.storage, + fileObject: fileContent, + fileHash: helloWorldHash, + }), + ).rejects.toThrow("You can't pin a file and upload it at the same time."); + + await expect( + store.Publish({ + channel: "TEST", + APIServer: DEFAULT_API_V2, + account: account, + storageEngine: ItemType.storage, + fileHash: helloWorldHash, + }), + ).rejects.toThrow("You must choose ipfs to pin file."); + + await expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store.Publish({ + channel: "TEST", + APIServer: DEFAULT_API_V2, + account: account, + storageEngine: ItemType.storage, + }), + ).rejects.toThrow("You need to specify a File to upload or a Hash to pin."); + }); });