Pinata の SDK を使って IPFS を操作する
ワークスペースの準備
mkdir hello-pinata-sdk
cd hello-pinata-sdk
npm init -y
npm install --save-dev @pinata/sdk dotenv @types/node ts-node
touch .env test-authentication.ts
Pinata にログインするのが久しぶりだ
相変わらずキリンみたいのが可愛い
My Files の表示には時間がかかる
ファイル一覧が表示されるまでに 5 秒くらいの時間がかかるみたい
反映されない!と焦らないように気をつけよう
API キーの作成
Settings > Developers タブ > New Key で追加できる
Admin にすると全ての API Endpoint Access が有効になるみたい
Limit Max Uses は API キーの使用回数を設定できるようだ
Key Name はわかりやすい名前にする、今回は hello-pinata-sdk にしよう
キーが作成されると下記 3 点が作成される
- API Key
- API Secret
- JWT
API Secret と JWT は二度と表示されないかも知れないので .env に忘れずに控えておく
PINATA_API_KEY="00000000000000000000"
PINATA_API_SECRET="0000000000000000000000000000000000000000000000000000000000000000"
PINATA_JWT="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
認証テスト
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const { authenticated } = await pinata.testAuthentication();
console.log(JSON.stringify({ authenticated }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config test-authentication.ts
{
"authenticated": true
}
認証は成功している様子だ
Pinata SDK を使ってできること
Pinning
- hashMetadata
- pinByHash
- pinFileToIPFS
- pinFromFS
- pinJobs
- pinJSONToIPFS
- unpin
- userPinPolicy
Data
- testAuthentication
- pinList
- getFilesByCount
- userPinnedDataTotal
色々あるけど
とりあえず pin 系の操作から試していこう
ファイルをピンする / pinFileToIPFS
ファイルというか Node.js のストリームをピンする
Pinata で「ピン」は IPFS にコンテンツをアップロードする+ダッシュボードから管理できるようにすることのようだ
touch pin-file.ts
import PinataClient from "@pinata/sdk";
import { Readable } from "stream";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const dataToPin = Readable.from('Hello, Pinata');
const pinResponse = await pinata.pinFileToIPFS(dataToPin, {
pinataMetadata: {
name: "myFirstPinnedData",
keyvalues: {
myKey: 'myValue',
} as any,
},
})
console.log(JSON.stringify({ pinResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config pin-file.ts
{
"pinResponse": {
"IpfsHash": "QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row",
"PinSize": 21,
"Timestamp": "2023-01-19T07:52:55.793Z"
}
}
Pinata のダッシュボードを見ても追加されていることがわかる、反映するにはリロードが必要
Name 列をクリックするとピンしたファイルの内容を閲覧できる
URL は https://gateway.pinata.cloud/ipfs/QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row
とりあえず今日はここまで、スムーズに進んで良かった
Pinata の料金体系
月額 $20 払えば 20,000 ファイルまでピンできる、いいね
pinFromFS
- ファイルのパスを指定してピンする
- ファイルだけではなくてディレクトリもできるようだ
- せっかくなのでディレクトリを試してみよう
mkdir directory-to-pin
echo 'Hello, pinFromFS!' > directory-to-pin/message.txt
touch pin-directory.ts
import PinataClient, { PinataMetadataFilter } from "@pinata/sdk";
import { join } from "path";
import { cwd } from "process";
async function main() {
const pinata = new PinataClient({
pinataJWTKey: process.env.PINATA_JWT!,
});
const directoryToPin = join(cwd(), "directory-to-pin");
const pinResponse = await pinata.pinFromFS(directoryToPin, {
pinataMetadata: {
name: "myFirstPinnedDirectory",
keyvalues: {
customKey: "customValue",
} as any,
},
});
console.log(JSON.stringify({ pinResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config pin-directory.ts
{
"pinResponse": {
"IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
"PinSize": 83,
"Timestamp": "2023-01-20T00:34:51.184Z"
}
}
- Pinata のダッシュボードからも確認できる
- ピンされたディレクトリの URL は下記の通り
- アクセスするとディレクトリ一覧が表示される
- ファイル名をクリックするとファイル内容が表示される
My Files の一覧を取得する API はあるのだろうか
あれば自作の管理画面とかを作るときに便利そうだ
クライアントサイドだけで使えるのだろうか
今のところ Node.js のストリームやファイルシステムを前提としているので難しいかな?
Pinata SDK は TypeScript 対応しているけど
- 微妙に整合性が取れていなくてもやもやする
- 例えば
pinataMetadata
にはas any
が必要
const pinResponse = await pinata.pinFromFS(directoryToPin, {
pinataMetadata: {
name: "myFirstPinnedDirectory",
keyvalues: {
customKey: "customValue",
} as any,
},
});
-
pinataMetadata
の型はPinataMetadata
で定義は下記の通り - これでは
keyvalues
にオブジェクトを指定するとエラーが出ても仕方がない
export interface PinataMetadata {
[key: string]: string | number | null;
}
-
PinataMetadataFilter
というのもあって似ているけどvalue
やop
が微妙に違う
export interface PinataMetadataFilter {
name?: string | undefined;
keyvalues: {
[key: string]: {
value: string | number | null;
op: string;
};
};
}
- もしかして自分の使い方が間違っている?
- 正しいと確信できればプルリクエストを送りたい
pin-directory.ts を再度実行してみる
- 同じ内容でピンしたらどうなるのか気になったので再度実行してみた
- 結果としては問題なくピンできた、ハッシュも同じでダッシュボードにも変化がない様子
{
"pinResponse": {
"IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
"PinSize": 83,
"Timestamp": "2023-01-20T00:34:51.184Z",
"isDuplicate": true
}
}
次は name
を変更して試してみる
pinataMetadata: {
name: "myFirstPinnedData2",
keyvalues: {
myKey: 'myValue',
} as any,
},
先ほどは気がつかなかったが "isDuplicate": true
が追加されている
{
"pinResponse": {
"IpfsHash": "QmZn26VASeHsaE8KUxd1dZWp2QUF9t3oL5oii5AdWbzs8V",
"PinSize": 83,
"Timestamp": "2023-01-20T00:34:51.184Z",
"isDuplicate": true
}
}
ダッシュボード上のファイル名に変化はない、最初の名前が優先されるようだ
Submarined って何だろう
ダッシュボードにある Submarined って何だろう、archived 的な何かな?
hashMetadata / メタデータ変更
touch change-metadata.ts
import "@pinata/sdk";
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const ipfsPinHash = "QmeTv51GpP6cMF3KoAPDYTrEqB9NUvqHygafgM3bNM9Row";
const changeMetadataResponse = await pinata.hashMetadata(ipfsPinHash, {
name: "myFirstPinnedData2",
keyvalues: {
customKey: null,
customKey2: "customValue2",
} as any,
});
console.log(JSON.stringify({ changeMetadataResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config change-metadata.ts
{
"changeMetadataResponse": "OK"
}
Pinata のダッシュボードで確認すると名前やタグが更新されたことがわかる
pinByHash
touch pin-by-hash.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const ipfsHashToPin = 'QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq';
const pinResponse = await pinata.pinByHash(ipfsHashToPin, {
pinataMetadata: {
name: "pinnedByHash",
},
})
console.log(JSON.stringify({ pinResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config pin-by-hash.ts
{
"pinResponse": {
"id": "84fe30c6-20e8-4cb8-8cef-e90a0d752ffe",
"ipfsHash": "QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq",
"status": "prechecking",
"name": "pinnedByHash"
}
}
-
id
はピンジョブの ID、ジョブとは IPFS 上でピンするコンテンツを探すジョブのことだろう -
ipfsHash
はピンする ipfsHash、ちなみに今回は Bored Ape Yacht Club にしてみた -
status
はピンジョブのステータス、リクエストが成功していればsearching
になるようだ -
name
はリクエスト時に指定した Pinata 上での名前(今回はpinnedByHash
)
Pinata のダッシュボード上にもちゃんと表示された
tokenId = 0 のファイルを開いてみる
image にアクセスしてみる
お猿の画像が表示された、ちなみに URL は下記の通り
BAYC のメタデータを見てて気がついたけど
NFT の tokenURI とか image とかに ipfs:
のアドレスを指定しても大丈夫なんだ
BAYC のように世界的に有名な NFT だからかも知れないけど
ピンとは何か?
Pinata 公式ドキュメントにしっかり書いてあった
When you “pin” data on an IPFS node, you are telling that node that the data is important and it should be saved. A node is a program that connects you to IPFS and stores files.
Pinning prevents important data from being deleted from your node. You and only you can control and pin data on your node(s)—you can not force other nodes on the IPFS network to pin your content for you. So, to guarantee your content stays pinned, you have to run your own IPFS nodes.
Once your file is pinned to IPFS, you have full control to share, distribute, monetize and share your files however you’d like.
せっかくなので自力で翻訳してみよう
IPFS ノードにデータをピンする時「このデータは重要なので保管してください」とノードに伝えています。ノードとはあなたを IPFS やファイルに接続するプログラムのことです。
ピンすることでノードから重要なデータが削除されることを防止できます。自分が管理するノードにデータをピンすることができますが、自分以外が管理するノードに対してデータをピンすることはできません。ピンしたいコンテンツが保管され続けるためには自分の IPFS ノードを運用する必要があります。
IPFS にファイルがピンされたら好きなようにファイルを共有・配布・収益化することができます。
Pinata は自分の代わりに IPFS ノードを運用してくれるサービスのようだ
pinJobs
未完了のピンジョブを一覧するための API
touch list-pin-jobs.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const pinJobs = await pinata.pinJobs();
console.log(JSON.stringify({ pinJobs }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config list-pin-jobs.ts
{
"pinJobs": {
"count": 0,
"rows": []
}
}
未完了のピンジョブが 0 件なのであまり面白くない結果になってしまった
ピンジョブが 1 件以上ある場合は次のようなフィールドが表示されるようだ
id
ipfs_pin_hash
date_queued
name
status
フィールド名がキャメルケースだったりスネークケースだったりするのが気になる
マイクロサービスとかで別々のチームが担当しているのだろうか
pinJSONToIPFS を試そうとしたら
面白いのが出てきた
{
reason: 'FORBIDDEN',
details: 'Account blocked due to plan usage limit'
}
もしかして BAYC をピンしたから?
Billing ページを見ると Number of items pinned が 10007 になっている
というかページ最上部にメッセージが表示されていた、何かあるなーとは思っていたけど
せっかくなので unpin を試してみよう
ピンされたアイテム数は増えているのにピンされたアイテムの合計サイズが増えていないのはなぜなのか気になる
そうか、ピンされているのはお猿の画像ファイルじゃなくてメタデータの JSON ファイルだからか、なるほど
unpin
touch unpin.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const ipfsHashToUnpin = 'QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq';
const unpinResponse = await pinata.unpin(ipfsHashToUnpin)
console.log(JSON.stringify({ unpinResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config unpin.ts
{
"unpinResponse": "OK"
}
My Files からは消えたけど赤いメッセージが出続けている
リロードしたら消えた、Billing ページにも反映されている
pinJSONToIPFS
touch pin-json.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const jsonToPin = {
message: "Hello, pinJSONToIPFS!",
};
const pinResponse = await pinata.pinJSONToIPFS(jsonToPin, {
pinataMetadata: {
name: "myFirstPinnedJSON",
},
});
console.log(JSON.stringify({ pinResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config pin-json.ts
{
"pinResponse": {
"IpfsHash": "QmZEYZ9fDkJDqmEteeYRdcRGgUrHv2ytxpsp4QTPEcSuNF",
"PinSize": 43,
"Timestamp": "2023-01-23T01:43:25.281Z"
}
}
My Files を見るとピンされていることがわかる
userPinPolicy
GitHub ページには userPinPolicy
という API もあるようなことが書かれているが実際には無いようだ
今日はここまでにして残りの Data 系の下記の API についても引き続き試していきたい
- pinList
- getFilesByCount
- userPinnedDataTotal
Submarining の説明が公式ドキュメントにあった
後から読みたい
pinList
- ピンされたファイルやディレクトリの一覧を表示する
- イメージとしてはダッシュボードの My Files に近い
touch list-pins.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const listResponse = await pinata.pinList({
pageLimit: 1,
});
console.log(JSON.stringify({ listResponse }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config list-pins.ts
{
"listResponse": {
"rows": [
{
"id": "1534de97-6d08-4356-a0bd-43c419b12419",
"ipfs_pin_hash": "QmZEYZ9fDkJDqmEteeYRdcRGgUrHv2ytxpsp4QTPEcSuNF",
"size": 43,
"user_id": "ca1b5bfe-5679-4190-95ee-37786427ddf8",
"date_pinned": "2023-01-23T01:43:25.281Z",
"date_unpinned": null,
"metadata": {
"name": "myFirstPinnedJSON",
"keyvalues": null
},
"regions": [
{
"regionId": "FRA1",
"currentReplicationCount": 1,
"desiredReplicationCount": 1
},
{
"regionId": "NYC1",
"currentReplicationCount": 1,
"desiredReplicationCount": 1
}
]
}
]
}
}
この API を使えば自前の My Files ページを作れそうだ
getFilesByCount
- 色々頑張ってみたけど期待した結果が得られない
- My Files のファイルを1つ1つ走査できることを期待したのだが
touch iterate-files.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const getFilesIterator = pinata.getFilesByCount({
status: "all",
});
for await (const getFileResponse of getFilesIterator) {
console.log(JSON.stringify({ getResponse: getFileResponse }, null, 2));
}
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config iterate-files.ts
(何も表示されない)
ソースコードを見てもただの便利メソッドのようなので必要なら自分で作ろうと思う
userPinnedDataTotal
- ピンされたファイルの合計バイト数を取得するメソッド
- 戻り値の型は
number
だが実際に取得してみるとオブジェクトだった - 合計バイト数に加えてピンされたファイル数とレプリケーションを含めた合計バイト数も同時に取得できる
touch get-total-bytes.ts
import PinataClient from "@pinata/sdk";
async function main() {
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_API_SECRET!,
});
const totalBytes = await pinata.userPinnedDataTotal();
console.log(JSON.stringify({ totalBytes }, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config get-total-bytes.ts
{
"totalBytes": {
"pin_count": 8,
"pin_size_total": 926752,
"pin_size_with_replications_total": 926752
}
}
Submarining について
特定の条件を満たす人にだけコンテンツを提供する機能、例えば下記
- ある NFT を持っている人
- リツイートしてくれた人
- ある地域に住んでいる人
特に NFT を所有している人にコンテンツを提供したいというニーズは多そう
ドキュメントは下記
下記の記事も面白い
CLI もある
公式ドキュメントを眺めていたら CLI があることを知った
例えばアップロードするだけなどプログラムを書くまでもないなら CLI で良さそう
$ npm install --save-dev pinata-upload-cli
$ npx pinata-cli --help
Usage: pinata-cli [options]
A command line tool to upload files and folders to Pinata
Options:
-V, --version output the version number
-a, --auth [jwt] API jwt from Pinata
-as --authSubmarine [api key] V2 API Key from Pinata
-u, --upload [file or folder] Source folder or file to upload to IPFS
-s, --submarine [file or folder] Source folder or file to submarine on
Pinata
-h, --help display help for command
$ source .env && npx pinata-cli --auth $PINATA_JWT
Authenticated
$ npx pinata-cli --upload pin-file.ts
{ percent: 0, transferred: 0, total: 802 }
{ percent: 0.1882793017456359, transferred: 151, total: 802 }
{ percent: 0.9276807980049875, transferred: 744, total: 802 }
{ percent: 1, transferred: 802, total: 802 }
Pinning, please wait...
{
IpfsHash: 'QmbEn3gdkj8HRBiC7o8iZFYBPhrEaWS152LRrh3mRKg22n',
PinSize: 604,
Timestamp: '2023-01-24T01:04:42.165Z'
}
おわりに
以上で一旦クローズ、Infura にも IPFS の API があるみたいなので機会を作って調べてみたい