Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add umi-uploader-cascade plugin #125

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/umi-uploader-cascade/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @metaplex-foundation/umi-uploader-cascade

## 0.0.1

### Patch Changes

- [`e84c9e`](https://github.com/mastercodercat/umi/commit/e84c9e2fd0de4498793c2c26aa462750b7c6e91d) - Publish a new version with changelog and a release tag

9 changes: 9 additions & 0 deletions packages/umi-uploader-cascade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# umi-uploader-cascade

An uploader implementation relying on Cascade Protocol.

## Installation

```sh
npm install @metaplex-foundation/umi-uploader-cascade
```
3 changes: 3 additions & 0 deletions packages/umi-uploader-cascade/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../babel.config.json"
}
70 changes: 70 additions & 0 deletions packages/umi-uploader-cascade/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@metaplex-foundation/umi-uploader-cascade",
"version": "0.1.1",
"description": "An uploader implementation relying on Cascade",
"license": "MIT",
"sideEffects": false,
"module": "dist/esm/index.mjs",
"main": "dist/cjs/index.cjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"files": [
"/dist/cjs",
"/dist/esm",
"/dist/types",
"/src"
],
"scripts": {
"lint": "eslint --ext js,ts,tsx src",
"lint:fix": "eslint --fix --ext js,ts,tsx src",
"clean": "rimraf dist",
"build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c",
"test": "ava"
},
"dependencies": {
"node-fetch": "^2.6.7"
},
"peerDependencies": {
"@metaplex-foundation/umi": "workspace:^"
},
"devDependencies": {
"@ava/typescript": "^3.0.1",
"@metaplex-foundation/umi": "workspace:^",
"@metaplex-foundation/umi-downloader-http": "workspace:^",
"@metaplex-foundation/umi-eddsa-web3js": "workspace:^",
"@metaplex-foundation/umi-http-fetch": "workspace:^",
"@metaplex-foundation/umi-rpc-web3js": "workspace:^",
"ava": "^5.1.0",
"typescript": "^4.5.4",
"@types/node-fetch": "^2.6.2",
"form-data": "^3.0.0"
},
"publishConfig": {
"access": "public"
},
"author": "Metaplex Maintainers <[email protected]>",
"homepage": "https://metaplex.com",
"repository": {
"url": "https://github.com/metaplex-foundation/umi.git"
},
"typedoc": {
"entryPoint": "./src/index.ts",
"readmeFile": "./README.md",
"displayName": "umi-uploader-cascade"
},
"ava": {
"typescript": {
"compile": false,
"rewritePaths": {
"src/": "dist/test/src/",
"test/": "dist/test/test/"
}
}
}
}
16 changes: 16 additions & 0 deletions packages/umi-uploader-cascade/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createConfigs } from '../../rollup.config';
import pkg from './package.json';

export default createConfigs({
pkg,
builds: [
{
dir: 'dist/esm',
format: 'es',
},
{
dir: 'dist/cjs',
format: 'cjs',
},
],
});
128 changes: 128 additions & 0 deletions packages/umi-uploader-cascade/src/createCascadeUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable no-await-in-loop */
import {
Context,
createGenericFileFromJson,
GenericFile,
lamports,
SolAmount,
UploaderInterface,
UploaderUploadOptions,
} from '@metaplex-foundation/umi';
import fetch from 'node-fetch';
const FormData = require('form-data');

const CASCADE_API_URL = 'https://gateway-api.pastel.network/';

export type CascadeUploaderOptions = {
apiKey: string;
};

export type CascadeUploadedItem = {
result_id: string;
result_status: string;
registration_ticket_txid: string | undefined;
original_file_ipfs_link: string | undefined;
error: string | undefined;
};

export type CascadeUploadResponse = {
request_id: string;
request_status: string;
results: CascadeUploadedItem[];
};

export function createCascadeUploader(
context: Pick<Context, 'rpc' | 'payer'>,
options: CascadeUploaderOptions = { apiKey: '' }
): UploaderInterface & {
upload2: (
files: GenericFile[],
options?: UploaderUploadOptions
) => Promise<CascadeUploadedItem[]>;
} {
const { apiKey } = options;

if (!apiKey) {
throw new Error('Cascade Gateway API key is required');
}

const getUploadPrice = async (): Promise<SolAmount> => lamports(0);

const upload = async (files: GenericFile[]): Promise<string[]> => {
const uris: string[] = [];

const body = new FormData();

files.forEach((file) => {
body.append('files', Buffer.from(file.buffer), file.fileName);
});

try {
const res = await fetch(
`${CASCADE_API_URL}/api/v1/cascade?make_publicly_accessible=true`,
{
headers: {
Api_key: apiKey,
},
method: 'POST',
body: body,
}
);

const data: CascadeUploadResponse = await res.json();
data.results.forEach((item) => {
if (item.original_file_ipfs_link)
uris.push(item.original_file_ipfs_link);
else {
uris.push('');
}
});
} catch (e) {
return [];
}
console.log(uris);
return uris;
};

const upload2 = async (
files: GenericFile[]
): Promise<CascadeUploadedItem[]> => {
const body = new FormData();

files.forEach((file) => {
body.append('files', Buffer.from(file.buffer), file.fileName);
});

try {
const res = await fetch(
`${CASCADE_API_URL}/api/v1/cascade?make_publicly_accessible=true`,
{
headers: {
Api_key: apiKey,
},
method: 'POST',
body,
}
);

const data: CascadeUploadResponse = await res.json();

return data.results;
} catch (e) {
return [];
}
};

const uploadJson = async <T>(json: T): Promise<string> => {
const file = createGenericFileFromJson(json);
const uris = await upload([file]);
return uris[0];
};

return {
getUploadPrice,
upload,
uploadJson,
upload2,
};
}
2 changes: 2 additions & 0 deletions packages/umi-uploader-cascade/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './createCascadeUploader';
export * from './plugin';
13 changes: 13 additions & 0 deletions packages/umi-uploader-cascade/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { UmiPlugin } from '@metaplex-foundation/umi';
import {
createCascadeUploader,
CascadeUploaderOptions,
} from './createCascadeUploader';

export const cascadeUploader = (
options?: CascadeUploaderOptions
): UmiPlugin => ({
install(umi) {
umi.uploader = createCascadeUploader(umi, options);
},
});
69 changes: 69 additions & 0 deletions packages/umi-uploader-cascade/test/CascadeUploader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Context,
createGenericFile,
createUmi,
generatedSignerIdentity,
utf8,
} from '@metaplex-foundation/umi';
import { httpDownloader } from '@metaplex-foundation/umi-downloader-http';
import { web3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js';
import { fetchHttp } from '@metaplex-foundation/umi-http-fetch';
import { web3JsRpc } from '@metaplex-foundation/umi-rpc-web3js';
import test from 'ava';
import { cascadeUploader, CascadeUploaderOptions } from '../src';

test('example test', async (t) => {
t.is(typeof cascadeUploader, 'function');
});

// TODO(loris): Unskip these tests when we can mock the Cascade API.

const getContext = (options?: CascadeUploaderOptions): Context =>
createUmi().use({
install(umi) {
umi.use(web3JsRpc('https://api.devnet.solana.com'));
umi.use(web3JsEddsa());
umi.use(fetchHttp());
umi.use(httpDownloader());
umi.use(generatedSignerIdentity());
umi.use(cascadeUploader(options));
},
});
// Use a dummy apiKey since the tests are skipped currently.
const apiKey = 'testKey';

test.skip('it can upload one file', async (t) => {
// Given a Context using Cascade.Storage.
const context = getContext({ apiKey });

// When we upload some asset.
const [uri] = await context.uploader.upload([
createGenericFile('some-image', 'some-image.jpg'),
]);

// Then the URI should be a valid IPFS URI.
t.truthy(uri);
t.true(uri.startsWith('https://ipfs.io/'));

// and it should point to the uploaded asset.
const [asset] = await context.downloader.download([uri]);
t.is(utf8.deserialize(asset.buffer)[0], 'some-image');
});

test.skip('it can upload multiple files in batch', async (t) => {
// Given a Context using Cascade with a batch size of 1.
const context = getContext({ apiKey });

// When we upload two assets.
const uris = await context.uploader.upload([
createGenericFile('some-image-A', 'some-image-A.jpg'),
createGenericFile('some-image-B', 'some-image-B.jpg'),
]);

// Then the URIs should point to the uploaded assets in the right order.
t.is(uris.length, 2);
const [assetA] = await context.downloader.download([uris[0]]);
t.is(utf8.deserialize(assetA.buffer)[0], 'some-image-A');
const [assetB] = await context.downloader.download([uris[1]]);
t.is(utf8.deserialize(assetB.buffer)[0], 'some-image-B');
});
11 changes: 11 additions & 0 deletions packages/umi-uploader-cascade/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.json",
"include": ["./**/*"],
"compilerOptions": {
"module": "commonjs",
"outDir": "../dist/test",
"declarationDir": null,
"declaration": false,
"emitDeclarationOnly": false
}
}
8 changes: 8 additions & 0 deletions packages/umi-uploader-cascade/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {
"outDir": "dist/esm",
"declarationDir": "dist/types"
}
}