Skip to content

Commit

Permalink
Merge pull request #125 from mastercodercat/cascade-uploader
Browse files Browse the repository at this point in the history
Add umi-uploader-cascade plugin
  • Loading branch information
blockiosaurus authored Jan 8, 2025
2 parents 44b060e + 16930e9 commit 8de0992
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/modern-zoos-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@metaplex-foundation/umi-uploader-cascade": patch
---

Add umi-uploader-cascade plugin
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",
"form-data": "^3.0.0"
},
"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"
},
"publishConfig": {
"access": "public"
},
"author": "Metaplex Maintainers <contact@metaplex.com>",
"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',
},
],
});
130 changes: 130 additions & 0 deletions packages/umi-uploader-cascade/src/createCascadeUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* 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,
}
);

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 [];
}
// eslint-disable-next-line no-console
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"
}
}
Loading

0 comments on commit 8de0992

Please sign in to comment.