Skip to content

Commit

Permalink
ALL-3061 NFT IPFS file upload (#979)
Browse files Browse the repository at this point in the history
* ALL-3061 Implemented ipfs file upload
* ALL-3061 Updated jest version
* ALL-3061 Mint nft in one action

---------

Co-authored-by: Oleksandr Loiko <[email protected]>
  • Loading branch information
alexloiko and Oleksandr Loiko authored Oct 13, 2023
1 parent 6734549 commit e3e2bb9
Show file tree
Hide file tree
Showing 13 changed files with 1,024 additions and 979 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
## [4.1.3] - 2023.10.12
### Added
- Added IPFS upload and NFT mint in one action. Metadata will be prepared and uploaded automatically

## [4.1.2] - 2023.10.12
### Added
- Added `walletProvider` to TatumSdkChain class so any chain can support wallet extensions.

## [4.1.1] - 2023.10.11
### Added
- Added RPC support for the SOLANA network. Users can now make RPC calls to these network using the `Network.SOLANA` network.
Expand Down
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tatumio/tatum",
"version": "4.1.2",
"version": "4.1.3",
"description": "Tatum JS SDK",
"author": "Tatum",
"repository": "https://github.com/tatumio/tatum-js",
Expand Down Expand Up @@ -33,18 +33,17 @@
"typedi": "^0.10.0"
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/jest": "^29.5.5",
"@types/node": "^18.15.11",
"@types/node-fetch": "^2.6.3",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"dotenv": "^16.0.3",
"eslint": "^8.14.0",
"jest": "27.0.0",
"jest": "^29.7.0",
"prettier": "^2.8.4",
"prettier-plugin-organize-imports": "^3.2.2",
"ts-jest": "^27.1.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.7.0",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
Expand Down
6 changes: 5 additions & 1 deletion src/connector/connector.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type DefaultParamsType = { [key: string]: string | number | boolean | undefined }
export type DefaultBodyType = object | object[]
export type DefaultBodyType = object | object[] | FormData

export interface GetUrl<PARAMS = DefaultParamsType> {
path?: string
Expand All @@ -11,3 +11,7 @@ export interface SdkRequest<PARAMS = DefaultParamsType, BODY = DefaultBodyType>
body?: BODY
method?: string
}

export interface FileUploadRequest<PARAMS = DefaultParamsType> extends SdkRequest<PARAMS, BlobPart> {
body: BlobPart
}
26 changes: 22 additions & 4 deletions src/connector/tatum.connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Container, Service } from 'typedi'
import { JsonRpcCall } from '../dto'
import { ApiVersion } from '../service'
import { CONFIG, Constant, Utils } from '../util'
import { DefaultBodyType, DefaultParamsType, GetUrl, SdkRequest } from './connector.dto'
import { DefaultBodyType, DefaultParamsType, FileUploadRequest, GetUrl, SdkRequest } from './connector.dto'

@Service({
factory: (data: { id: string }) => {
Expand Down Expand Up @@ -31,6 +31,15 @@ export class TatumConnector {
return this.request<RESPONSE>({ ...request, method: 'DELETE' })
}

public async uploadFile<RESPONSE>(request: FileUploadRequest) {
const formData = new FormData()
formData.append('file', new Blob([request.body]))
return this.request<RESPONSE>(
{ ...request, method: 'POST', body: formData, basePath: Constant.TATUM_API_URL.V3 },
0,
)
}

private async request<
RESPONSE,
PARAMS extends DefaultParamsType = DefaultParamsType,
Expand All @@ -41,11 +50,20 @@ export class TatumConnector {
externalUrl?: string,
): Promise<RESPONSE> {
const url = externalUrl || this.getUrl({ path, params, basePath })
const headers = await Utils.getHeaders(this.id)
const isUpload = body && body instanceof FormData
const headers = isUpload ? Utils.getBasicHeaders(this.id) : Utils.getHeaders(this.id)

let requestBody: string | FormData | null = null
if (isUpload) {
requestBody = body
} else if (body) {
requestBody = JSON.stringify(body)
}

const request: RequestInit = {
headers,
headers: headers,
method,
body: body ? JSON.stringify(body) : null,
body: requestBody,
}

const start = Date.now()
Expand Down
2 changes: 2 additions & 0 deletions src/service/ipfs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ipfs'
export * from './ipfs.dto'
6 changes: 6 additions & 0 deletions src/service/ipfs/ipfs.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UploadFile {
/**
* Data to be uploaded
*/
file: BlobPart
}
35 changes: 35 additions & 0 deletions src/service/ipfs/ipfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Container, Service } from 'typedi'
import { TatumConnector } from '../../connector/tatum.connector'
import { CONFIG, ErrorUtils, ResponseDto } from '../../util'
import { TatumConfig } from '../tatum'
import { UploadFile } from './ipfs.dto'

@Service({
factory: (data: { id: string }) => {
return new Ipfs(data.id)
},
transient: true,
})
export class Ipfs {
protected readonly connector: TatumConnector
protected readonly config: TatumConfig

constructor(private readonly id: string) {
this.config = Container.of(this.id).get(CONFIG)
this.connector = Container.of(this.id).get(TatumConnector)
}

/**
* Upload file to the IPFS storage.
* @param body Body of the request with file to be uploaded.
* @returns ResponseDto<{txId: string}> IPFS hash id of the uploaded file.
*/
async uploadFile(body: UploadFile): Promise<ResponseDto<{ ipfsHash: string }>> {
return ErrorUtils.tryFail(() =>
this.connector.uploadFile<{ ipfsHash: string }>({
path: `ipfs`,
body: body.file,
}),
)
}
}
25 changes: 21 additions & 4 deletions src/service/nft/nft.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ export interface MintNft {
* Address to send NFT to
*/
to: string
/**
* The URL pointing to the NFT metadata; for more information, see EIP-721
*/
url: string
/**
* Smart contract address of the NFT collection
*/
Expand All @@ -63,6 +59,27 @@ export interface MintNft {
minter?: string
}

export interface MintNftWithUrl extends MintNft {
/**
* The URL pointing to the NFT metadata; for more information, see EIP-721
*/
url: string
}

export interface MintNftWithMetadata extends MintNft {
/**
* File to be uploaded as NFT metadata
*/
file: BlobPart
/**
* NFT metadata to be stored on IPFS along with the file
*/
metadata: {
name: string
description?: string
} & Record<string, unknown>
}

export interface MetadataResponse {
url: string
metadata: object
Expand Down
47 changes: 45 additions & 2 deletions src/service/nft/nft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ApiBalanceRequest } from '../../api/api.dto'
import { TatumConnector } from '../../connector/tatum.connector'
import { AddressBalanceDetails } from '../../dto'
import { CONFIG, ErrorUtils, ResponseDto } from '../../util'
import { Ipfs } from '../ipfs'
import { TatumConfig } from '../tatum'
import {
CheckTokenOwner,
Expand All @@ -14,7 +15,8 @@ import {
GetCollection,
GetNftMetadata,
GetTokenOwner,
MintNft,
MintNftWithMetadata,
MintNftWithUrl,
NftAddressBalance,
NftTokenDetail,
NftTransaction,
Expand Down Expand Up @@ -65,10 +67,12 @@ export class NftTezos {
export class Nft {
private readonly connector: TatumConnector
private readonly config: TatumConfig
private readonly ipfs: Ipfs

constructor(private readonly id: string) {
this.config = Container.of(this.id).get(CONFIG)
this.connector = Container.of(this.id).get(TatumConnector)
this.ipfs = Container.of(this.id).get(Ipfs)
}

/**
Expand Down Expand Up @@ -120,7 +124,7 @@ export class Nft {
* @param body Body of the request.
* @returns ResponseDto<{txId: string}> Transaction ID of the mint transaction. {
*/
async mintNft(body: MintNft): Promise<ResponseDto<{ txId: string }>> {
async mintNft(body: MintNftWithUrl): Promise<ResponseDto<{ txId: string }>> {
return ErrorUtils.tryFail(() =>
this.connector.post<{ txId: string }>({
path: `contract/erc721/mint`,
Expand All @@ -132,6 +136,45 @@ export class Nft {
)
}

/**
* Mint new NFT (using ERC-721 compatible smart contract).
* This operation uploads file to IPFS, prepares and uploads metadata to IPFS and mints nft using prepared metadata's IPFS url.
* You don't need to specify the default minter of the collection, as the owner of the collection is the default minter.
* You don't have to have any funds on the address, as the nft is minted by Tatum.
* @param body Body of the request.
* @returns ResponseDto<{txId: string}> Transaction ID of the mint transaction. {
*/
async mintNftWithMetadata(body: MintNftWithMetadata): Promise<ResponseDto<{ txId: string }>> {
const imageUpload = await this.ipfs.uploadFile({ file: body.file })
if (imageUpload.error) {
return ErrorUtils.toErrorResponse(imageUpload.error)
}

const metadataUpload = await this.ipfs.uploadFile({
file: Buffer.from(
JSON.stringify({
...body.metadata,
image: `ipfs://${imageUpload.data.ipfsHash}`,
}),
),
})

if (metadataUpload.error) {
return ErrorUtils.toErrorResponse(metadataUpload.error)
}

return ErrorUtils.tryFail(() =>
this.connector.post<{ txId: string }>({
path: `contract/erc721/mint`,
body: {
...body,
url: `ipfs://${metadataUpload.data.ipfsHash}`,
chain: this.config.network,
},
}),
)
}

/**
* Get balance of NFT for given address.
* You can get balance of multiple addresses in one call.
Expand Down
3 changes: 3 additions & 0 deletions src/service/tatum/tatum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '../extensions'
import { Faucet } from '../faucet'
import { FeeEvm, FeeUtxo } from '../fee'
import { Ipfs } from '../ipfs'
import { Nft, NftTezos } from '../nft'
import { Notification } from '../notification'
import { Rates } from '../rate'
Expand Down Expand Up @@ -85,6 +86,7 @@ export class BaseTatumSdk extends TatumSdkChain {
address: Address
rates: Rates
faucet: Faucet
ipfs: Ipfs

constructor(id: string) {
super(id)
Expand All @@ -94,6 +96,7 @@ export class BaseTatumSdk extends TatumSdkChain {
this.address = Container.of(id).get(Address)
this.rates = Container.of(id).get(Rates)
this.faucet = Container.of(id).get(Faucet)
this.ipfs = Container.of(id).get(Ipfs)
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/util/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,11 @@ export const ErrorUtils = {
typeof (e as Record<string, unknown>).message === 'string'
)
},
toErrorResponse<T>(error: ErrorWithMessage): ResponseDto<T> {
return {
data: null as unknown as T,
status: Status.ERROR,
error: error,
}
},
}
11 changes: 8 additions & 3 deletions src/util/util.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
isEvmArchiveNonArchiveLoadBalancerNetwork,
isEvmBasedNetwork,
isEvmLoadBalancerNetwork,
isNativeEvmLoadBalancerNetwork, isSameGetBlockNetwork,
isNativeEvmLoadBalancerNetwork,
isSameGetBlockNetwork,
isSolanaNetwork,
isTronLoadBalancerNetwork,
isTronNetwork,
Expand Down Expand Up @@ -72,10 +73,10 @@ import { TronLoadBalancerRpc } from '../service/rpc/evm/TronLoadBalancerRpc'
import { TronRpc } from '../service/rpc/evm/TronRpc'
import { EosLoadBalancerRpc } from '../service/rpc/other/EosLoadBalancerRpc'
import { EosRpc } from '../service/rpc/other/EosRpc'
import { SolanaLoadBalancerRpc } from '../service/rpc/other/SolanaLoadBalancerRpc'
import { XrpLoadBalancerRpc } from '../service/rpc/other/XrpLoadBalancerRpc'
import { Constant } from './constant'
import { CONFIG } from './di.tokens'
import { SolanaLoadBalancerRpc } from '../service/rpc/other/SolanaLoadBalancerRpc'

export const Utils = {
getRpc: <T>(id: string, config: TatumConfig): T => {
Expand Down Expand Up @@ -336,9 +337,13 @@ export const Utils = {
return JSON.stringify(headersObj)
},
getHeaders: (id: string) => {
const basicHeaders = Utils.getBasicHeaders(id)
basicHeaders.set('Content-Type', 'application/json')
return basicHeaders
},
getBasicHeaders: (id: string) => {
const config = Container.of(id).get(CONFIG)
const headers = new Headers({
'Content-Type': 'application/json',
'x-ttm-sdk-version': version,
'x-ttm-sdk-product': 'JS',
'x-ttm-sdk-debug': `${config.verbose}`,
Expand Down
Loading

0 comments on commit e3e2bb9

Please sign in to comment.