Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 46 additions & 30 deletions apps/cross-chain-accounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

This example demonstrates how one can upload and view files using cross-chain (non-Aptos) wallets on the Shelby network.

Checkout the live deployed dapp on:
https://shelby-x-chain-accounts-example.vercel.app/

### 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>`
- Head to [Geomi](https://geomi.dev/) to generate an API key for `shelbynet` and add it to the `.env` file as `NEXT_PUBLIC_SHELBY_API_KEY=<my-api-key>`

### Starting the Demo dApp

Expand Down Expand Up @@ -43,54 +46,63 @@ We then send all these hashes to the blockchain so they can be verified with the

```ts
export const encodeFile = async (file: File): Promise<BlobCommitments> => {
// Make sure data is a Buffer
const data = Buffer.isBuffer(file)
? file
: Buffer.from(await file.arrayBuffer());

const commitments = await generateCommitments(data);
// Create provider for direct use with generateCommitments
const provider = await ClayErasureCodingProvider.create();

// Generate a commitment
const commitments = await generateCommitments(provider, data);

return commitments;
};
```

#### Register the file on chain (transaction submission)
#### Register the file on the Aptos 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
> Note: To upload a file to the Shelby network, the account should hold shelbyUSD tokens (1 shelbyUSD for 1 upload). Make sure to fund your account by going to https://docs.shelby.xyz/apis/faucet/shelbyusd

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({
const payload = ShelbyBlobClient.createRegisterBlobPayload({
account: account.address,
blobName: file.name,
blobMerkleRoot: commitment.blob_merkle_root,
chunksetChunkCommitments: commitment.chunkset_commitments.map(
(chunkset) => chunkset.chunk_commitments
),
numChunksets: expectedTotalChunksets(commitment.raw_data_size),
expirationMicros: (1000 * 60 * 60 * 24 * 30 + Date.now()) * 1000, // 30 days from now in microseconds
size: commitment.raw_data_size,
blobSize: 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.
Once we have the transaction payload, we can submit it to the Aptos 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
> Note: Make sure your wallet is configured to use the `shelbynet` 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
> - Node URL: https://api.shelbynet.shelby.xyz/v1
> - Faucet URL: https://faucet.shelbynet.shelby.xyz (APT faucet)
> - Indexer URL: https://api.shelbynet.shelby.xyz/v1/graphql

```ts
import {
type InputTransactionData,
useWallet,
} from "@aptos-labs/wallet-adapter-react";

const { signAndSubmitTransaction } = useWallet();
// 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({
await getAptosClient().waitForTransaction({
transactionHash: transactionSubmitted.hash,
});
```
Expand All @@ -100,6 +112,13 @@ await getShelbyClient().aptos.waitForTransaction({
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
import {
type InputTransactionData,
useWallet,
} from "@aptos-labs/wallet-adapter-react";

const { signTransaction } = useWallet();

// Create the sponsor account
const privateKey = new Ed25519PrivateKey(
PrivateKey.formatPrivateKey(
Expand All @@ -110,9 +129,9 @@ const privateKey = new Ed25519PrivateKey(
const sponsorAccount = Account.fromPrivateKey({ privateKey });

// Build the transaction
const rawTransaction = await getShelbyClient().aptos.transaction.build.simple({
const rawTransaction = await getAptosClient().transaction.build.simple({
sender: account.address,
data: payload,
data: payload, // the payload we generated in the previous step
withFeePayer: true,
});

Expand All @@ -122,23 +141,20 @@ const walletSignedTransaction = await signTransaction({
});

// Sponsor signs the transaction
const sponsorAuthenticator = getShelbyClient().aptos.transaction.signAsFeePayer(
{
signer: sponsorAccount,
transaction: rawTransaction,
}
);
const sponsorAuthenticator = getAptosClient().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,
});
const transactionSubmitted = await getAptosClient().transaction.submit.simple({
transaction: rawTransaction,
senderAuthenticator: walletSignedTransaction.authenticator,
feePayerAuthenticator: sponsorAuthenticator,
});

// Wait for transaction to be submitted on the chain
await getShelbyClient().aptos.waitForTransaction({
await getAptosClient().waitForTransaction({
transactionHash: transactionSubmitted.hash,
});
```
Expand Down
80 changes: 55 additions & 25 deletions apps/cross-chain-accounts/components/AccountBlobs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import type { BlobMetadata } from "@shelby-protocol/sdk/browser";
import Image from "next/image";
import { useEffect, useState } from "react";
import { getShelbyClient, SHELBY_API_URL } from "@/utils/client";
import { getShelbyClient } from "@/utils/client";

interface AccountBlobsProps {
refreshTrigger?: number;
Expand Down Expand Up @@ -29,6 +30,16 @@ export const AccountBlobs = ({ refreshTrigger }: AccountBlobsProps) => {
});
}, [account, refreshTrigger]);

const extractFileName = (blobName: string): string => {
return blobName.split("/").pop() || blobName;
};

const isImageFile = (filename: string): boolean => {
const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp"];
const extension = filename.split(".").pop()?.toLowerCase();
return extension ? imageExtensions.includes(extension) : false;
};

return (
<div className="border border-gray-200 dark:border-gray-700 rounded-xl p-6 bg-background">
{!account && (
Expand All @@ -54,29 +65,49 @@ export const AccountBlobs = ({ refreshTrigger }: AccountBlobsProps) => {
{/* 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);
}}
/>
{isImageFile(extractFileName(blob.name)) ? (
<Image
src={`${
process.env.NEXT_PUBLIC_SHELBY_API_URL
}/v1/blobs/${blob.owner.toString()}/${extractFileName(
blob.name,
)}`}
alt={blob.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
borderRadius: "4px",
}}
width={100}
height={100}
onError={(e) => {
console.error("Image failed to load:", e);
}}
/>
) : (
<div
className="w-full h-full flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded"
style={{
borderRadius: "4px",
}}
>
<div className="text-center p-4">
<div className="text-gray-400 dark:text-gray-500 mb-2" />
<p className="text-sm font-medium text-gray-600 dark:text-gray-300 break-words">
{extractFileName(blob.name)}
</p>
</div>
</div>
)}
</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}
{extractFileName(blob.name)}
</h3>

<div className="space-y-2 text-sm">
Expand All @@ -90,18 +121,17 @@ export const AccountBlobs = ({ refreshTrigger }: AccountBlobsProps) => {
</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
}`}
href={`${
process.env.NEXT_PUBLIC_SHELBY_API_URL
}/v1/blobs/${blob.owner.toString()}/${extractFileName(
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
Download Image
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/cross-chain-accounts/components/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export const FileUpload = ({ onUploadSuccess }: FileUploadProps) => {
</label>
<input
id="file-upload"
accept="image/*"
type="file"
accept="image/*, .pdf, .txt, .doc, .docx, .xls, .xlsx, .ppt, .pptx"
onChange={handleFileChange}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200 dark:file:bg-gray-800 dark:file:text-gray-300 dark:hover:file:bg-gray-700"
/>
Expand Down
8 changes: 4 additions & 4 deletions apps/cross-chain-accounts/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { XChainWalletSelector } from "@shelby-protocol/ui/components/x-chain-wal
export const Header = () => {
const { connected, account } = useWallet();

const onMintProto = () => {
const onMintShelbyUsd = () => {
if (!account) {
return;
}
window.open(
`https://docs.shelby.xyz/docs/faucet?address=${account.address}`,
`https://docs.shelby.xyz/apis/faucet/shelbyusd?address=${account.address}`,
"_blank",
);
};
Expand All @@ -23,8 +23,8 @@ export const Header = () => {
</h1>
</div>
<div className="flex items-center gap-3">
<Button disabled={!connected} onClick={() => onMintProto()}>
Mint PROTO
<Button disabled={!connected} onClick={() => onMintShelbyUsd()}>
Mint shelbyUSD
</Button>
<XChainWalletSelector
size="sm"
Expand Down
13 changes: 6 additions & 7 deletions apps/cross-chain-accounts/hooks/useSubmitFileToChain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
} from "@aptos-labs/wallet-adapter-react";
import {
type BlobCommitments,
expectedTotalChunksets,
ShelbyBlobClient,
} from "@shelby-protocol/sdk/browser";
import { useCallback, useState } from "react";
import { getShelbyClient } from "@/utils/client";
import { getAptosClient, getShelbyClient } from "@/utils/client";

interface UseSubmitFileToChainReturn {
submitFileToChain: (commitment: BlobCommitments, file: File) => Promise<void>;
Expand All @@ -37,15 +38,13 @@ export const useSubmitFileToChain = (): UseSubmitFileToChainReturn => {
setError(null);

try {
const payload = ShelbyBlobClient.createWriteBlobCommitmentsPayload({
const payload = ShelbyBlobClient.createRegisterBlobPayload({
account: account.address,
blobName: file.name,
blobMerkleRoot: commitment.blob_merkle_root,
chunksetChunkCommitments: commitment.chunkset_commitments.map(
(chunkset) => chunkset.chunk_commitments,
),
numChunksets: expectedTotalChunksets(commitment.raw_data_size),
expirationMicros: (1000 * 60 * 60 * 24 * 30 + Date.now()) * 1000, // 30 days from now in microseconds
size: commitment.raw_data_size,
blobSize: commitment.raw_data_size,
});

if (wallet.isAptosNativeWallet) {
Expand All @@ -55,7 +54,7 @@ export const useSubmitFileToChain = (): UseSubmitFileToChainReturn => {
const transactionSubmitted =
await signAndSubmitTransaction(transaction);

await getShelbyClient().aptos.waitForTransaction({
await getAptosClient().waitForTransaction({
transactionHash: transactionSubmitted.hash,
});
} else {
Expand Down
9 changes: 8 additions & 1 deletion apps/cross-chain-accounts/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ const nextConfig: NextConfig = {
transpilePackages: ["@shelby-protocol/ui"],
typedRoutes: true,
images: {
remotePatterns: [new URL("https://api.devnet.shelby.xyz/shelby")],
remotePatterns: [
{
protocol: "https",
hostname: "api.shelbynet.shelby.xyz",
port: "",
pathname: "/**",
},
],
},
};

Expand Down
4 changes: 2 additions & 2 deletions apps/cross-chain-accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"dependencies": {
"@aptos-labs/derived-wallet-ethereum": "^0.8.0",
"@aptos-labs/derived-wallet-solana": "^0.8.0",
"@aptos-labs/ts-sdk": "^5.1.0",
"@aptos-labs/ts-sdk": "^5.1.1",
"@aptos-labs/wallet-adapter-react": "^7.1.0",
"@shelby-protocol/sdk": "0.0.1-experimental.4",
"@shelby-protocol/sdk": "0.0.3",
"@shelby-protocol/ui": "workspace:*",
"next": "^15.5.0",
"react": "^18.3.1",
Expand Down
Loading