-
Notifications
You must be signed in to change notification settings - Fork 61
Add cross chain accounts example #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
File renamed without changes.
This file contains hidden or 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,171 @@ | ||
| # Shelby Cross-Chain Accounts Example | ||
|
|
||
| This example demonstrates how one can upload and view files using cross-chain (non-Aptos) wallets on the Shelby network. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| - `node` and `pnpm` | ||
| - Shelby API key. For best performance, you are encouraged to generate an API key so your app will not hit the rate limit. | ||
| - Head to [Geomi](https://geomi.dev/) to generate an API key for `shelbydevnet` and add it to the `.env` file as `NEXT_PUBLIC_SHELBY_API_KEY=<my-api-key>` | ||
|
|
||
| ### Starting the Demo dApp | ||
|
|
||
| Create an `.env` file by copying the `.env.example` file and fill out the required variables. | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| Then install dependencies and run the dapp locally | ||
|
|
||
| ```bash | ||
| pnpm install | ||
| pnpm run dev | ||
| ``` | ||
|
|
||
| ### Implementation Details | ||
|
|
||
| The dApp provides a flow to connect an Aptos native or cross-chain wallet, upload a static file to the Shelby network, and view the account's uploaded files through a user interface. | ||
|
|
||
| ### Upload a File | ||
|
|
||
| At a high level, uploading a file to Shelby includes 3 steps: | ||
|
|
||
| 1. Encode the File | ||
| 2. Register the file on chain (transaction submission) | ||
| 3. Upload the file to Shelby RPC | ||
|
|
||
| #### Encode File | ||
|
|
||
| Encoding a file means we split the file into chunks, where each chunk has a `commitment hash`, and these are combined to make the `blob merkle root hash`. | ||
|
|
||
| We then send all these hashes to the blockchain so they can be verified with the storage providers. | ||
|
|
||
| ```ts | ||
| export const encodeFile = async (file: File): Promise<BlobCommitments> => { | ||
| const data = Buffer.isBuffer(file) | ||
| ? file | ||
| : Buffer.from(await file.arrayBuffer()); | ||
|
|
||
| const commitments = await generateCommitments(data); | ||
|
|
||
| return commitments; | ||
| }; | ||
| ``` | ||
|
|
||
| #### Register the file on chain (transaction submission) | ||
|
|
||
| > Note: To upload a file to the Shelby network, the account should hold PROTO tokens (1 PROTO for 1 upload). Make sure to fund your account by going to https://docs.shelby.xyz/docs/faucet | ||
|
|
||
| After we have the commitment hashes of the file, we can register the file on chain by submitting a transaction. | ||
|
|
||
| ```ts | ||
| // Generate the transaction payload | ||
| const payload = ShelbyBlobClient.createWriteBlobCommitmentsPayload({ | ||
| account: account.address, | ||
| blobName: file.name, | ||
| blobMerkleRoot: commitment.blob_merkle_root, | ||
| chunksetChunkCommitments: commitment.chunkset_commitments.map( | ||
| (chunkset) => chunkset.chunk_commitments | ||
| ), | ||
| expirationMicros: (1000 * 60 * 60 * 24 * 30 + Date.now()) * 1000, // 30 days from now in microseconds | ||
| size: commitment.raw_data_size, | ||
| }); | ||
| ``` | ||
|
|
||
| Once we have the transaction payload, we can submit it to the chain. As mentioned before, in this example we support both Aptos native and cross-chain wallet transaction submissions. | ||
|
|
||
| ##### Aptos native wallets | ||
|
|
||
| > Note: Make sure your wallet is configured to use the `shelbydevnet` network. Petra (and some other wallets) lets you create a custom network, use those values | ||
| > | ||
| > - Node URL: https://api.devnet.shelby.xyz/v1 | ||
| > - Faucet URL: https://faucet.devnet.shelby.xyz (APT faucet) | ||
| > - Indexer URL: https://api.devnet.shelby.xyz/v1/graphql | ||
|
|
||
| ```ts | ||
| // Send the transaction to the connected wallet to sign and submit | ||
| const transaction: InputTransactionData = { | ||
| data: payload, | ||
| }; | ||
| const transactionSubmitted = await signAndSubmitTransaction(transaction); | ||
| // Wait for transaction to be submitted on the chain | ||
| await getShelbyClient().aptos.waitForTransaction({ | ||
| transactionHash: transactionSubmitted.hash, | ||
| }); | ||
| ``` | ||
|
|
||
| ##### Cross-chain wallets | ||
|
|
||
| Following the [cross-chain wallet docs](https://aptos.dev/build/sdks/wallet-adapter/x-chain-accounts#submitting-a-transaction), it is recommended to sponsor the transaction as we can assume a cross-chain wallet does not have APT to pay the transaction fees. | ||
|
|
||
| ```ts | ||
| // Create the sponsor account | ||
| const privateKey = new Ed25519PrivateKey( | ||
| PrivateKey.formatPrivateKey( | ||
| process.env.NEXT_PUBLIC_SPONSOR_PRIVATE_KEY as string, | ||
| PrivateKeyVariants.Ed25519 | ||
| ) | ||
| ); | ||
| const sponsorAccount = Account.fromPrivateKey({ privateKey }); | ||
|
|
||
| // Build the transaction | ||
| const rawTransaction = await getShelbyClient().aptos.transaction.build.simple({ | ||
| sender: account.address, | ||
| data: payload, | ||
| withFeePayer: true, | ||
| }); | ||
|
|
||
| // Send the transaction to the connected (cross-chain) wallet to sign | ||
| const walletSignedTransaction = await signTransaction({ | ||
| transactionOrPayload: rawTransaction, | ||
| }); | ||
|
|
||
| // Sponsor signs the transaction | ||
| const sponsorAuthenticator = getShelbyClient().aptos.transaction.signAsFeePayer( | ||
| { | ||
| signer: sponsorAccount, | ||
| transaction: rawTransaction, | ||
| } | ||
| ); | ||
|
|
||
| // Submit the transaction to chain | ||
| const transactionSubmitted = | ||
| await getShelbyClient().aptos.transaction.submit.simple({ | ||
| transaction: rawTransaction, | ||
| senderAuthenticator: walletSignedTransaction.authenticator, | ||
| feePayerAuthenticator: sponsorAuthenticator, | ||
| }); | ||
|
|
||
| // Wait for transaction to be submitted on the chain | ||
| await getShelbyClient().aptos.waitForTransaction({ | ||
| transactionHash: transactionSubmitted.hash, | ||
| }); | ||
| ``` | ||
|
|
||
| #### Upload the file to Shelby RPC | ||
|
|
||
| After we submit the transaction and register the commitment hashes on the chain, we can upload the file to the Shelby RPC so it can be verified with the storage providers to ensure that the uploaded data matches the one on the blockchain by comparing the hashes. | ||
|
|
||
| > Note: The RPC will make checks on-chain to ensure the file is there first, which is why registration can't happen in parallel with an upload to the RPC. | ||
|
|
||
| ```ts | ||
| await getShelbyClient().rpc.putBlob({ | ||
| account: account.address, | ||
| blobName: file.name, | ||
| blobData: new Uint8Array(await file.arrayBuffer()), | ||
| }); | ||
| ``` | ||
|
|
||
| ### View the account's uploaded files | ||
|
|
||
| To view the files uploaded by an account, we can simply query for the blobs. | ||
|
|
||
| ```ts | ||
| const getBlobs = async (): Promise<BlobMetadata[]> => { | ||
| const blobs = await getShelbyClient().coordination.getAccountBlobs({ | ||
| account: account.address, | ||
| }); | ||
| return blobs; | ||
| }; | ||
| ``` |
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains hidden or 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 @@ | ||
| @import "@shelby-protocol/ui/globals.css"; |
This file contains hidden or 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
This file contains hidden or 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,28 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { AccountBlobs } from "@/components/AccountBlobs"; | ||
| import { FileUpload } from "@/components/FileUpload"; | ||
| import { Header } from "@/components/Header"; | ||
|
|
||
| export default function Home() { | ||
| const [refreshTrigger, setRefreshTrigger] = useState(0); | ||
|
|
||
| const handleUploadSuccess = () => { | ||
| setRefreshTrigger((prev) => prev + 1); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="min-h-screen p-5"> | ||
| <Header /> | ||
|
|
||
| {/* Main Content */} | ||
| <main className="flex-1"> | ||
| <div className="max-w-4xl mx-auto space-y-6"> | ||
| <FileUpload onUploadSuccess={handleUploadSuccess} /> | ||
| <AccountBlobs refreshTrigger={refreshTrigger} /> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or 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,20 @@ | ||
| { | ||
| "$schema": "https://ui.shadcn.com/schema.json", | ||
| "style": "new-york", | ||
| "rsc": true, | ||
| "tsx": true, | ||
| "tailwind": { | ||
| "config": "", | ||
| "css": "../../packages/ui/src/styles/globals.css", | ||
| "baseColor": "neutral", | ||
| "cssVariables": true | ||
| }, | ||
| "iconLibrary": "lucide", | ||
| "aliases": { | ||
| "components": "@/components", | ||
| "hooks": "@/hooks", | ||
| "lib": "@/lib", | ||
| "utils": "@shelby-protocol/ui/lib/utils", | ||
| "ui": "@shelby-protocol/ui/components" | ||
| } | ||
| } |
This file contains hidden or 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,114 @@ | ||
| import { useWallet } from "@aptos-labs/wallet-adapter-react"; | ||
| import type { BlobMetadata } from "@shelby-protocol/sdk/browser"; | ||
| import { useEffect, useState } from "react"; | ||
| import { getShelbyClient, SHELBY_API_URL } from "@/utils/client"; | ||
|
|
||
| interface AccountBlobsProps { | ||
| refreshTrigger?: number; | ||
| } | ||
|
|
||
| export const AccountBlobs = ({ refreshTrigger }: AccountBlobsProps) => { | ||
| const { account } = useWallet(); | ||
| const [blobs, setBlobs] = useState<BlobMetadata[]>([]); | ||
|
|
||
| useEffect(() => { | ||
| if (!account) { | ||
| setBlobs([]); | ||
| return; | ||
| } | ||
| const getBlobs = async (): Promise<BlobMetadata[]> => { | ||
| const blobs = await getShelbyClient().coordination.getAccountBlobs({ | ||
| account: account.address, | ||
| }); | ||
| return blobs; | ||
| }; | ||
|
|
||
| getBlobs().then((blobs) => { | ||
| setBlobs(blobs); | ||
| refreshTrigger; | ||
| }); | ||
| }, [account, refreshTrigger]); | ||
|
|
||
| return ( | ||
| <div className="border border-gray-200 dark:border-gray-700 rounded-xl p-6 bg-background"> | ||
| {!account && ( | ||
| <div className="text-center py-8"> | ||
| <p className="text-gray-500 dark:text-gray-400"> | ||
| Please connect your wallet to view blobs | ||
| </p> | ||
| </div> | ||
| )} | ||
| {account && blobs.length === 0 && ( | ||
| <div className="text-center py-8"> | ||
| <p className="text-gray-500 dark:text-gray-400"> | ||
| No blobs found for this account. Upload a file to get started! | ||
| </p> | ||
| </div> | ||
| )} | ||
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||
| {blobs.map((blob) => ( | ||
| <div | ||
| key={blob.name} | ||
| className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden" | ||
| > | ||
| {/* Image Section */} | ||
| <div className="w-full h-48 bg-gray-100 dark:bg-gray-700 p-2"> | ||
| <div className="h-full relative"> | ||
| <img | ||
| src={`${SHELBY_API_URL}/v1/blobs/${blob.owner.toString()}/${ | ||
| blob.name | ||
| }`} | ||
| alt={blob.name} | ||
| style={{ | ||
| width: "100%", | ||
| height: "100%", | ||
| objectFit: "cover", | ||
| display: "block", | ||
| borderRadius: "4px", | ||
| }} | ||
| onError={(e) => { | ||
| console.error("Image failed to load:", e); | ||
| }} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Content Section */} | ||
| <div className="p-4 space-y-3"> | ||
| <h3 className="font-semibold text-lg text-gray-900 dark:text-white truncate"> | ||
| {blob.name} | ||
| </h3> | ||
|
|
||
| <div className="space-y-2 text-sm"> | ||
| <div> | ||
| <span className="text-gray-500 dark:text-gray-400"> | ||
| Owner: | ||
| </span> | ||
| <p className="font-mono text-xs bg-gray-50 dark:bg-gray-700 p-1 rounded mt-1 break-all"> | ||
| {blob.owner.toString()} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div> | ||
| <span className="text-gray-500 dark:text-gray-400"> | ||
| Download: | ||
| </span> | ||
| <a | ||
| href={`${SHELBY_API_URL}/v1/blobs/${blob.owner.toString()}/${ | ||
| blob.name | ||
| }`} | ||
| className="block text-blue-600 dark:text-blue-400 hover:underline text-xs mt-1 break-all" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| View Image | ||
| </a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.