undedoloremque
Version:
Green Field JS SDK
791 lines (699 loc) • 24 kB
text/typescript
import { encodePath, getMsgToSign, getSortQuery, secpSign } from '../clients/spclient/auth';
import { getApprovalMetaInfo } from '../clients/spclient/spApis/approval';
import { getGetObjectMetaInfo } from '../clients/spclient/spApis/getObject';
import {
getObjectMetaInfo,
parseGetObjectMetaResponse,
} from '../clients/spclient/spApis/getObjectMeta';
import {
getListObjectPoliciesMetaInfo,
parseGetListObjectPoliciesResponse,
} from '../clients/spclient/spApis/listObjectPolicies';
import { parseListObjectsByBucketNameResponse } from '../clients/spclient/spApis/listObjectsByBucket';
import {
getListObjectsByIDsMetaInfo,
parseListObjectsByIdsResponse,
} from '../clients/spclient/spApis/listObjectsByIds';
import { parseError } from '../clients/spclient/spApis/parseError';
import { getPutObjectMetaInfo } from '../clients/spclient/spApis/putObject';
import { TxClient } from '../clients/txClient';
import { METHOD_GET, NORMAL_ERROR_CODE } from '../constants/http';
import { MsgCancelCreateObjectSDKTypeEIP712 } from '../messages/greenfield/storage/MsgCancelCreateObject';
import { getMsgCreateObjectSDKTypeEIP712 } from '../messages/greenfield/storage/MsgCreateObject';
import { MsgDeleteObjectSDKTypeEIP712 } from '../messages/greenfield/storage/MsgDeleteObject';
import { MsgUpdateObjectInfoSDKTypeEIP712 } from '../messages/greenfield/storage/MsgUpdateObjectInfo';
import { signSignatureByEddsa } from '../offchainauth';
import { GetObjectRequest } from '../types/sp/GetObject';
import { GetObjectMetaRequest, GetObjectMetaResponse } from '../types/sp/GetObjectMeta';
import { ListObjectsByBucketNameResponse } from '../types/sp/ListObjectsByBucketName';
import { PutObjectRequest } from '../types/sp/PutObject';
import {
ActionType,
Principal,
PrincipalType,
principalTypeFromJSON,
} from '@bnb-chain/greenfield-cosmos-types/greenfield/permission/common';
import {
redundancyTypeFromJSON,
visibilityTypeFromJSON,
} from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common';
import {
QueryHeadObjectResponse,
QueryNFTRequest,
QueryObjectNFTResponse,
QueryPolicyForAccountResponse,
QueryVerifyPermissionResponse,
} from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/query';
import {
MsgCancelCreateObject,
MsgCreateObject,
MsgDeleteObject,
MsgDeletePolicy,
MsgPutPolicy,
MsgUpdateObjectInfo,
} from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/tx';
import { bytesFromBase64 } from '@bnb-chain/greenfield-cosmos-types/helpers';
import { hexlify } from '@ethersproject/bytes';
import { Headers } from 'cross-fetch';
import { bytesToUtf8, hexToBytes, utf8ToBytes } from 'ethereum-cryptography/utils';
import { container, delay, inject, injectable } from 'tsyringe';
import {
GRNToString,
MsgCancelCreateObjectTypeUrl,
MsgCreateObjectTypeUrl,
MsgDeleteObjectTypeUrl,
MsgUpdateObjectInfoTypeUrl,
newObjectGRN,
} from '..';
import { RpcQueryClient } from '../clients/queryclient';
import { AuthType, SpClient } from '../clients/spclient/spClient';
import {
CreateObjectApprovalRequest,
CreateObjectApprovalResponse,
GetListObjectPoliciesRequest,
GetListObjectPoliciesResponse,
GetPrivewObject,
ListObjectsByBucketNameRequest,
ListObjectsByIDsRequest,
ListObjectsByIDsResponse,
Long,
SpResponse,
TxResponse,
} from '../types';
import {
generateUrlByBucketName,
isValidBucketName,
isValidObjectName,
isValidUrl,
} from '../utils/s3';
import { Sp } from './sp';
import { Storage } from './storage';
import { ResourceTags } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/types';
export interface IObject {
getCreateObjectApproval(
configParam: CreateObjectApprovalRequest,
authType: AuthType,
): Promise<SpResponse<string>>;
createObject(
getApprovalParams: CreateObjectApprovalRequest,
authType: AuthType,
): Promise<TxResponse>;
uploadObject(configParam: PutObjectRequest, authType: AuthType): Promise<SpResponse<null>>;
cancelCreateObject(msg: MsgCancelCreateObject): Promise<TxResponse>;
updateObjectInfo(msg: MsgUpdateObjectInfo): Promise<TxResponse>;
deleteObject(msg: MsgDeleteObject): Promise<TxResponse>;
headObject(bucketName: string, objectName: string): Promise<QueryHeadObjectResponse>;
headObjectById(objectId: string): Promise<QueryHeadObjectResponse>;
headObjectNFT(request: QueryNFTRequest): Promise<QueryObjectNFTResponse>;
/**
* get s3 object's blob
*/
getObject(configParam: GetObjectRequest, authType: AuthType): Promise<SpResponse<Blob>>;
getObjectPreviewUrl(configParam: GetPrivewObject, authType: AuthType): Promise<string>;
/**
* download s3 object
*/
downloadFile(configParam: GetObjectRequest, authType: AuthType): Promise<void>;
listObjects(
configParam: ListObjectsByBucketNameRequest,
): Promise<SpResponse<ListObjectsByBucketNameResponse>>;
createFolder(
getApprovalParams: Omit<
CreateObjectApprovalRequest,
'contentLength' | 'fileType' | 'expectCheckSums'
>,
authType: AuthType,
): Promise<TxResponse>;
putObjectPolicy(
bucketName: string,
objectName: string,
// expirationTime: Date,
srcMsg: Omit<MsgPutPolicy, 'resource' | 'expirationTime'>,
): Promise<TxResponse>;
deleteObjectPolicy(
operator: string,
bucketName: string,
objectName: string,
principalAddr: string,
principalType: keyof typeof PrincipalType,
): Promise<TxResponse>;
isObjectPermissionAllowed(
bucketName: string,
objectName: string,
actionType: ActionType,
operator: string,
): Promise<QueryVerifyPermissionResponse>;
getObjectPolicy(
bucketName: string,
objectName: string,
principalAddr: string,
): Promise<QueryPolicyForAccountResponse>;
getObjectMeta(params: GetObjectMetaRequest): Promise<SpResponse<GetObjectMetaResponse>>;
listObjectsByIds(params: ListObjectsByIDsRequest): Promise<SpResponse<ListObjectsByIDsResponse>>;
listObjectPolicies(
params: GetListObjectPoliciesRequest,
): Promise<SpResponse<GetListObjectPoliciesResponse>>;
}
@injectable()
export class Objects implements IObject {
constructor(
@inject(delay(() => TxClient)) private txClient: TxClient,
@inject(delay(() => Storage)) private storage: Storage,
@inject(delay(() => Sp)) private sp: Sp,
) {}
private queryClient: RpcQueryClient = container.resolve(RpcQueryClient);
private spClient = container.resolve(SpClient);
public async getCreateObjectApproval(params: CreateObjectApprovalRequest, authType: AuthType) {
const {
bucketName,
creator,
objectName,
visibility = 'VISIBILITY_TYPE_PUBLIC_READ',
duration = 3000,
fileType = 'application/octet-stream',
redundancyType = 'REDUNDANCY_EC_TYPE',
contentLength,
expectCheckSums,
tags,
} = params;
try {
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidObjectName(objectName)) {
throw new Error('Error object name');
}
if (!creator) {
throw new Error('empty creator address');
}
let endpoint = params.endpoint;
if (!endpoint) {
endpoint = await this.sp.getSPUrlByBucket(bucketName);
}
const { reqMeta, optionsWithOutHeaders, url } =
getApprovalMetaInfo<CreateObjectApprovalResponse>(endpoint, 'CreateObject', {
bucket_name: bucketName,
content_type: fileType,
creator: creator,
expect_checksums: expectCheckSums,
object_name: objectName,
payload_size: contentLength.toString(),
primary_sp_approval: {
expired_height: '0',
global_virtual_group_family_id: 0,
sig: null,
},
redundancy_type: redundancyType,
visibility,
tags,
});
const signHeaders = await this.spClient.signHeaders(reqMeta, authType);
const result = await this.spClient.callApi(
url,
{
...optionsWithOutHeaders,
headers: signHeaders,
},
duration,
{
code: -1,
message: 'Get create object approval error.',
},
);
const signedMsgString = result.headers.get('X-Gnfd-Signed-Msg') || '';
const signedMsg = JSON.parse(
bytesToUtf8(hexToBytes(signedMsgString)),
) as CreateObjectApprovalResponse;
return {
code: 0,
message: 'Get create object approval success.',
body: result.headers.get('X-Gnfd-Signed-Msg') ?? '',
statusCode: result.status,
signedMsg,
};
} catch (error: any) {
throw {
code: -1,
message: error.message,
statusCode: error?.statusCode || NORMAL_ERROR_CODE,
};
}
}
private async createObjectTx(msg: MsgCreateObject, signedMsg: CreateObjectApprovalResponse) {
const isTagsEmpty = msg?.tags?.tags?.length === 0;
const MsgCreateObjectSDKTypeEIP712 = getMsgCreateObjectSDKTypeEIP712(isTagsEmpty);
return await this.txClient.tx(
MsgCreateObjectTypeUrl,
msg.creator,
MsgCreateObjectSDKTypeEIP712,
{
...signedMsg,
type: MsgCreateObjectTypeUrl,
primary_sp_approval: {
expired_height: signedMsg.primary_sp_approval.expired_height,
global_virtual_group_family_id:
signedMsg.primary_sp_approval.global_virtual_group_family_id,
sig: signedMsg.primary_sp_approval.sig,
},
},
MsgCreateObject.encode(msg).finish(),
);
}
public async createObject(getApprovalParams: CreateObjectApprovalRequest, authType: AuthType) {
const { signedMsg } = await this.getCreateObjectApproval(getApprovalParams, authType);
if (!signedMsg) {
throw new Error('Get create object approval error');
}
const msg: MsgCreateObject = {
bucketName: signedMsg.bucket_name,
creator: signedMsg.creator,
objectName: signedMsg.object_name,
contentType: signedMsg.content_type,
payloadSize: Long.fromString(signedMsg.payload_size),
visibility: visibilityTypeFromJSON(signedMsg.visibility),
expectChecksums: signedMsg.expect_checksums.map((e: string) => bytesFromBase64(e)),
redundancyType: redundancyTypeFromJSON(signedMsg.redundancy_type),
primarySpApproval: {
expiredHeight: Long.fromString(signedMsg.primary_sp_approval.expired_height),
sig: bytesFromBase64(signedMsg.primary_sp_approval.sig || ''),
globalVirtualGroupFamilyId: signedMsg.primary_sp_approval.global_virtual_group_family_id,
},
tags: ResourceTags.fromJSON(signedMsg.tags),
};
return await this.createObjectTx(msg, signedMsg);
}
public async uploadObject(
params: PutObjectRequest,
authType: AuthType,
): Promise<SpResponse<null>> {
const { bucketName, objectName, txnHash, body, duration = 30000 } = params;
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidObjectName(objectName)) {
throw new Error('Error object name');
}
if (!txnHash) {
throw new Error('Transaction hash is empty, please check.');
}
let endpoint = params.endpoint;
if (!endpoint) {
endpoint = await this.sp.getSPUrlByBucket(bucketName);
}
const { reqMeta, optionsWithOutHeaders, url } = await getPutObjectMetaInfo(endpoint, {
bucketName,
objectName,
contentType: body.type,
txnHash,
body,
});
const signHeaders = await this.spClient.signHeaders(reqMeta, authType);
try {
const result = await this.spClient.callApi(
url,
{
...optionsWithOutHeaders,
headers: signHeaders,
},
duration,
);
const { status } = result;
return { code: 0, message: 'Put object success.', statusCode: status };
} catch (error: any) {
return {
code: -1,
message: error.message,
statusCode: error?.statusCode || NORMAL_ERROR_CODE,
};
}
}
public async cancelCreateObject(msg: MsgCancelCreateObject) {
return await this.txClient.tx(
MsgCancelCreateObjectTypeUrl,
msg.operator,
MsgCancelCreateObjectSDKTypeEIP712,
MsgCancelCreateObject.toSDK(msg),
MsgCancelCreateObject.encode(msg).finish(),
);
}
public async deleteObject(msg: MsgDeleteObject) {
return await this.txClient.tx(
MsgDeleteObjectTypeUrl,
msg.operator,
MsgDeleteObjectSDKTypeEIP712,
MsgDeleteObject.toSDK(msg),
MsgDeleteObject.encode(msg).finish(),
);
}
public async updateObjectInfo(msg: MsgUpdateObjectInfo) {
return await this.txClient.tx(
MsgUpdateObjectInfoTypeUrl,
msg.operator,
MsgUpdateObjectInfoSDKTypeEIP712,
MsgUpdateObjectInfo.toSDK(msg),
MsgUpdateObjectInfo.encode(msg).finish(),
);
}
public async headObject(bucketName: string, objectName: string) {
const rpc = await this.queryClient.getStorageQueryClient();
return rpc.HeadObject({
bucketName,
objectName,
});
}
public async headObjectById(objectId: string) {
const rpc = await this.queryClient.getStorageQueryClient();
return rpc.HeadObjectById({
objectId,
});
}
public async headObjectNFT(request: QueryNFTRequest) {
const rpc = await this.queryClient.getStorageQueryClient();
return await rpc.HeadObjectNFT(request);
}
public async getObject(params: GetObjectRequest, authType: AuthType) {
try {
const { bucketName, objectName, duration = 30000 } = params;
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidObjectName(objectName)) {
throw new Error('Error object name');
}
let endpoint = params.endpoint;
if (!endpoint) {
endpoint = await this.sp.getSPUrlByBucket(bucketName);
}
const { reqMeta, optionsWithOutHeaders, url } = await getGetObjectMetaInfo(endpoint, {
bucketName,
objectName,
});
const headers = await this.spClient.signHeaders(reqMeta, authType);
const result = await this.spClient.callApi(
url,
{
...optionsWithOutHeaders,
headers,
},
duration,
);
const { status } = result;
if (!result.ok) {
const xmlError = await result.text();
const { code, message } = await parseError(xmlError);
return {
code: code || -1,
message: message || 'Get object error.',
statusCode: status,
};
}
const fileBlob = await result.blob();
return {
code: 0,
body: fileBlob,
message: 'Get object success.',
statusCode: status,
};
} catch (error: any) {
return {
code: -1,
message: error.message,
statusCode: error?.statusCode || NORMAL_ERROR_CODE,
};
}
}
public async getObjectPreviewUrl(params: GetPrivewObject, authType: AuthType) {
const { bucketName, objectName, queryMap } = params;
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidObjectName(objectName)) {
throw new Error('Error object name');
}
let endpoint = params.endpoint;
if (!endpoint) {
endpoint = await this.sp.getSPUrlByBucket(bucketName);
}
const path = '/' + encodePath(objectName);
const url = generateUrlByBucketName(endpoint, bucketName) + path;
const queryRaw = getSortQuery(queryMap);
const canonicalRequest = [
METHOD_GET,
`/${encodePath(objectName)}`,
queryRaw,
new URL(url).host,
'\n',
].join('\n');
const unsignedMsg = getMsgToSign(utf8ToBytes(canonicalRequest));
let authorization = '';
if (authType.type === 'ECDSA') {
const sig = secpSign(unsignedMsg, authType.privateKey);
authorization = `GNFD1-ECDSA, Signature=${sig.slice(2)}`;
} else {
const sig = await signSignatureByEddsa(authType.seed, hexlify(unsignedMsg).slice(2));
authorization = `GNFD1-EDDSA,Signature=${sig}`;
}
return `${url}?Authorization=${encodeURIComponent(authorization)}&${queryRaw}`;
}
public async downloadFile(configParam: GetObjectRequest, authType: AuthType): Promise<void> {
try {
const { objectName } = configParam;
const getObjectResult = await this.getObject(configParam, authType);
if (getObjectResult.code !== 0) {
throw new Error(getObjectResult.message);
}
const file = getObjectResult?.body;
if (file) {
// const {file} = getObjectResult;
const fileURL = URL.createObjectURL(file);
// create <a> tag dynamically
const fileLink = document.createElement('a');
fileLink.href = fileURL;
// it forces the name of the downloaded file
fileLink.download = objectName as string;
// triggers the click event
fileLink.click();
}
return;
} catch (error: any) {
throw new Error(error);
}
}
public async listObjects(configParam: ListObjectsByBucketNameRequest) {
try {
const { bucketName, endpoint, duration = 30000, query = new URLSearchParams() } = configParam;
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidUrl(endpoint)) {
throw new Error('Invalid endpoint');
}
const url = `${generateUrlByBucketName(endpoint, bucketName)}?${query?.toString()}`;
const headers = new Headers();
const result = await this.spClient.callApi(
url,
{
headers,
method: METHOD_GET,
},
duration,
);
const { status } = result;
if (!result.ok) {
const xmlError = await result.text();
const { code, message } = await parseError(xmlError);
return {
code: code || -1,
message: message || 'List object error.',
statusCode: status,
};
}
const xmlData = await result.text();
const res = await parseListObjectsByBucketNameResponse(xmlData);
return {
code: 0,
message: 'List object success.',
statusCode: status,
body: res,
};
} catch (error: any) {
return {
code: -1,
message: error.message,
statusCode: error?.statusCode || NORMAL_ERROR_CODE,
};
}
}
public async createFolder(
getApprovalParams: Omit<
CreateObjectApprovalRequest,
'contentLength' | 'fileType' | 'expectCheckSums'
>,
authType: AuthType,
) {
if (!getApprovalParams.objectName.endsWith('/')) {
throw new Error(
'failed to create folder. Folder names must end with a forward slash (/) character',
);
}
/**
* const file = new File([], 'scc', { type: 'text/plain' });
const fileBytes = await file.arrayBuffer();
console.log('fileBytes', fileBytes);
const hashResult = await FileHandler.getPieceHashRoots(new Uint8Array(fileBytes));
console.log('hashResult', hashResult);
const { contentLength, expectCheckSums } = hashResult;
*/
const params: CreateObjectApprovalRequest = {
bucketName: getApprovalParams.bucketName,
objectName: getApprovalParams.objectName,
contentLength: 0,
fileType: 'text/plain',
expectCheckSums: [
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
'47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
],
creator: getApprovalParams.creator,
tags: getApprovalParams.tags,
};
return this.createObject(params, authType);
}
public async putObjectPolicy(
bucketName: string,
objectName: string,
// expirationTime: Date,
srcMsg: Omit<MsgPutPolicy, 'resource' | 'expirationTime'>,
) {
const resource = GRNToString(newObjectGRN(bucketName, objectName));
const msg: MsgPutPolicy = {
...srcMsg,
resource,
// expirationTime: fromJsonTimestamp(expirationTime),
};
return await this.storage.putPolicy(msg);
}
public async isObjectPermissionAllowed(
bucketName: string,
objectName: string,
actionType: ActionType,
operator: string,
) {
const rpc = await this.queryClient.getStorageQueryClient();
return await rpc.VerifyPermission({
bucketName,
objectName,
actionType,
operator,
});
}
public async getObjectPolicy(bucketName: string, objectName: string, principalAddr: string) {
const rpc = await this.queryClient.getStorageQueryClient();
const resource = GRNToString(newObjectGRN(bucketName, objectName));
return await rpc.QueryPolicyForAccount({
resource,
principalAddress: principalAddr,
});
}
public async deleteObjectPolicy(
operator: string,
bucketName: string,
objectName: string,
principalAddr: string,
principalType: keyof typeof PrincipalType,
) {
const resource = GRNToString(newObjectGRN(bucketName, objectName));
const principal: Principal = {
type: principalTypeFromJSON(principalType),
value: principalAddr,
};
const msg: MsgDeletePolicy = {
resource,
principal,
operator: operator,
};
return await this.storage.deletePolicy(msg);
}
public async getObjectMeta(params: GetObjectMetaRequest) {
const { bucketName, objectName, endpoint } = params;
if (!isValidBucketName(bucketName)) {
throw new Error('Error bucket name');
}
if (!isValidObjectName(objectName)) {
throw new Error('Error object name');
}
const { url } = getObjectMetaInfo(endpoint, params);
const result = await this.spClient.callApi(url, {
method: METHOD_GET,
});
const xml = await result.text();
const res = await parseGetObjectMetaResponse(xml);
return {
code: 0,
message: 'get object meta success.',
statusCode: result.status,
body: res,
};
}
public async listObjectsByIds(params: ListObjectsByIDsRequest) {
try {
const sp = await this.sp.getInServiceSP();
const { url } = getListObjectsByIDsMetaInfo(sp.endpoint, params);
const result = await this.spClient.callApi(
url,
{
headers: {},
method: METHOD_GET,
},
3000,
);
const { status } = result;
if (!result.ok) {
const xmlError = await result.text();
const { code, message } = await parseError(xmlError);
throw {
code: code || -1,
message: message || 'error',
statusCode: status,
};
}
const xmlData = await result.text();
const res = await parseListObjectsByIdsResponse(xmlData);
return {
code: 0,
message: 'success',
statusCode: status,
body: res,
};
} catch (error: any) {
return {
code: -1,
message: error.message,
statusCode: error?.statusCode || NORMAL_ERROR_CODE,
};
}
}
public async listObjectPolicies(params: GetListObjectPoliciesRequest) {
let endpoint = params.endpoint;
if (!endpoint) {
endpoint = await this.sp.getSPUrlByBucket(params.bucketName);
}
const { url } = getListObjectPoliciesMetaInfo(endpoint, params);
const result = await this.spClient.callApi(url, {
headers: {},
method: METHOD_GET,
});
const xml = await result.text();
const res = parseGetListObjectPoliciesResponse(xml);
return {
code: 0,
message: 'success',
statusCode: result.status,
body: res,
};
}
}