-
Notifications
You must be signed in to change notification settings - Fork 295
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
title: IPFS Pinning With Crust | ||
|
||
## IPFS | ||
Algorand offers [various ways to store data in contracts](docs/get-details/dapps/smart-contracts/apps/state.md), but there are still many use cases where storing the data off-chain makes more sense. This is paticularly true when the data is large and not used directly on-chain (for example, NFT metadata and images). A common solution for off-chain data storage is the InterPlanetary File System (IPFS) protocol. In short, IPFS is a peer-to-peer file sharing protocol. For more information on IPFS, see https://docs.ipfs.tech/concepts/faq/. | ||
|
||
In order to share files via IPFS, one must pin a file on the network. Pinning a file means assigning it a unique Content Identifier (CID) and making it availible to download. It is common for developers to use a pinning service like Pinata, web3.storage, or nft.storage. While these services do indeed pin the file on IPFS, they are still using centralized servers to do so. This means those using these services are dependend on them to keep running them and are locked into their pricing model. | ||
|
||
|
||
## Crust | ||
|
||
To avoid using centralized services for IPFS pinning, Algorand developers can use the Crust network. Crust is a decentralized pinning service where users can pay the network to pin a file and that file will be pinned on many servers around the world. The pricing model is set by the node runners, rather than a single entity. For more information on Crust, see https://crust.network/faq/. | ||
|
||
## Crust and Algorand | ||
|
||
Crust is easier than ever to use for Algorand developers because you can pay for storage via ABI method calls to the Crust contracts deployed on testnet and mainnet. | ||
|
||
### Deployments | ||
Testnet storage contract application ID: [507867511](https://testnet.explorer.perawallet.app/application/507867511/) | ||
|
||
Mainnet storage contract application ID: [1275319623](https://explorer.perawallet.app/application/1275319623/) | ||
|
||
|
||
### Usage | ||
|
||
The easiest way to use the Crust storage contract is to use the ARC32 `application.json` that was generated by the beaker contract. The JSON and full source can be found at https://github.com/crustio/algorand-storage-contract. | ||
|
||
The general process is: | ||
|
||
1. Build web3 authentication header | ||
2. Upload files to IPFS | ||
3. Get storage price | ||
4. Place storage order | ||
|
||
#### Building Header | ||
|
||
```ts | ||
function getAuthHeader(account: algosdk.Account) { | ||
const sk32 = account.sk.slice(0, 32) | ||
const signingKey = nacl.sign.keyPair.fromSeed(sk32) | ||
|
||
const signature = nacl.sign(Buffer.from(account.addr), signingKey.secretKey) | ||
const sigHex = Buffer.from(signature).toString('hex').slice(0, 128) | ||
const authStr = `sub-${account.addr}:0x${sigHex}` | ||
|
||
return Buffer.from(authStr).toString('base64') | ||
} | ||
``` | ||
|
||
#### Upload to IPFS | ||
|
||
```ts | ||
async function uploadToIPFS(account: algosdk.Account) { | ||
const headers = { "Authorization": `Basic ${getAuthHeader(account)}`, "Content-Disposition": `form-data; name="upload_file"; filename="README.md"` } | ||
|
||
const response = await fetch('https://gw-seattle.crustcloud.io:443/api/v0/add', { | ||
method: 'POST', | ||
headers: { | ||
...headers, | ||
'Content-Type': 'multipart/form-data; boundary=ae36a08c478c4b29b6491c99272fe367', | ||
}, | ||
body: '--ae36a08c478c4b29b6491c99272fe367\nContent-Disposition: form-data; name="upload_file"; filename="README.md"\n\n# crust-examples\n\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.\n\n--ae36a08c478c4b29b6491c99272fe367--\n' | ||
}); | ||
|
||
const json: any = await response.json() | ||
|
||
return { cid: json.Hash, size: Number(json.Size) } | ||
} | ||
``` | ||
|
||
#### Get Storage Price | ||
|
||
```ts | ||
async function getPrice(algod: algosdk.Algodv2, appClient: StorageOrderClient, size: number) { | ||
const result = await (await appClient.compose().getPrice({ size, is_permanent: false }).atc()).simulate(algod) | ||
|
||
return result.methodResults[0].returnValue?.valueOf() as number | ||
} | ||
``` | ||
|
||
#### Place Order | ||
|
||
```ts | ||
async function getOrderNode(algod: algosdk.Algodv2, appClient: StorageOrderClient) { | ||
return (await (await appClient.compose().getRandomOrderNode({}, { boxes: [new Uint8Array(Buffer.from('nodes'))] }).atc()).simulate(algod)).methodResults[0].returnValue?.valueOf() as string | ||
} | ||
|
||
async function placeOrder(algod: algosdk.Algodv2, appClient: StorageOrderClient, account: algosdk.Account, cid: string, size: number, price: number) { | ||
|
||
const merchant = await getOrderNode(algod, appClient) | ||
const seed = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ | ||
from: account.addr, | ||
to: (await appClient.appClient.getAppReference()).appAddress, | ||
amount: price, | ||
suggestedParams: await algod.getTransactionParams().do(), | ||
}); | ||
|
||
appClient.placeOrder({ seed, cid, size, is_permanent: false, merchant }) | ||
} | ||
``` | ||
|
||
#### Full Example | ||
```ts | ||
import * as algokit from '@algorandfoundation/algokit-utils'; | ||
import { StorageOrderClient } from './StorageOrderClient' | ||
import algosdk from 'algosdk'; | ||
import nacl from 'tweetnacl' | ||
|
||
async function getAccount(algod: algosdk.Algodv2) { | ||
const kmd = algokit.getAlgoKmdClient({ | ||
server: 'http://localhost', | ||
port: 4002, | ||
token: 'a'.repeat(64), | ||
}); | ||
|
||
// Use algokit to create a KMD account named 'deployer' | ||
const account = await algokit.getOrCreateKmdWalletAccount({ | ||
name: 'uploader', | ||
// set fundWith to 0 so algokit doesn't try to fund the account from another kmd account | ||
fundWith: algokit.microAlgos(0), | ||
}, algod, kmd); | ||
|
||
const { amount } = await algod.accountInformation(account.addr).do(); | ||
|
||
if (amount === 0) { | ||
throw Error(`Account ${account.addr} has no funds. Please fund it and try again.`); | ||
} | ||
|
||
return account | ||
} | ||
|
||
async function getAppClient(algod: algosdk.Algodv2, sender: algosdk.Account, network: 'testnet' | 'mainnet') { | ||
|
||
|
||
return new StorageOrderClient( | ||
{ | ||
sender, | ||
resolveBy: 'id', | ||
id: network === 'testnet' ? 507867511 : 1275319623, | ||
}, | ||
algod, | ||
); | ||
} | ||
|
||
async function getPrice(algod: algosdk.Algodv2, appClient: StorageOrderClient, size: number) { | ||
const result = await (await appClient.compose().getPrice({ size, is_permanent: false }).atc()).simulate(algod) | ||
|
||
return result.methodResults[0].returnValue?.valueOf() as number | ||
} | ||
|
||
function getAuthHeader(account: algosdk.Account) { | ||
const sk32 = account.sk.slice(0, 32) | ||
const signingKey = nacl.sign.keyPair.fromSeed(sk32) | ||
|
||
const signature = nacl.sign(Buffer.from(account.addr), signingKey.secretKey) | ||
const sigHex = Buffer.from(signature).toString('hex').slice(0, 128) | ||
const authStr = `sub-${account.addr}:0x${sigHex}` | ||
|
||
return Buffer.from(authStr).toString('base64') | ||
} | ||
|
||
async function uploadToIPFS(account: algosdk.Account) { | ||
const headers = { "Authorization": `Basic ${getAuthHeader(account)}`, "Content-Disposition": `form-data; name="upload_file"; filename="README.md"` } | ||
|
||
const response = await fetch('https://gw-seattle.crustcloud.io:443/api/v0/add', { | ||
method: 'POST', | ||
headers: { | ||
...headers, | ||
'Content-Type': 'multipart/form-data; boundary=ae36a08c478c4b29b6491c99272fe367', | ||
}, | ||
body: '--ae36a08c478c4b29b6491c99272fe367\nContent-Disposition: form-data; name="upload_file"; filename="README.md"\n\n# crust-examples\n\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run index.ts\n```\n\nThis project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.\n\n--ae36a08c478c4b29b6491c99272fe367--\n' | ||
}); | ||
|
||
const json: any = await response.json() | ||
|
||
return { cid: json.Hash, size: Number(json.Size) } | ||
} | ||
|
||
async function getOrderNode(algod: algosdk.Algodv2, appClient: StorageOrderClient) { | ||
return (await (await appClient.compose().getRandomOrderNode({}, { boxes: [new Uint8Array(Buffer.from('nodes'))] }).atc()).simulate(algod)).methodResults[0].returnValue?.valueOf() as string | ||
} | ||
|
||
async function placeOrder(algod: algosdk.Algodv2, appClient: StorageOrderClient, account: algosdk.Account, cid: string, size: number, price: number) { | ||
|
||
const merchant = await getOrderNode(algod, appClient) | ||
const seed = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ | ||
from: account.addr, | ||
to: (await appClient.appClient.getAppReference()).appAddress, | ||
amount: price, | ||
suggestedParams: await algod.getTransactionParams().do(), | ||
}); | ||
|
||
appClient.placeOrder({ seed, cid, size, is_permanent: false, merchant }) | ||
} | ||
|
||
async function main(network: 'testnet' | 'mainnet') { | ||
const algod = algokit.getAlgoClient(algokit.getAlgoNodeConfig(network, 'algod')); | ||
const account = await getAccount(algod) | ||
|
||
const appClient = await getAppClient(algod, account, network) | ||
|
||
const { size, cid } = await uploadToIPFS(account) | ||
|
||
const price = await getPrice(algod, appClient, size) | ||
|
||
await placeOrder(algod, appClient, account, cid, size, price) | ||
} | ||
|
||
main('testnet') | ||
``` |